Skip to content

Commit 1ce3457

Browse files
authored
Merge pull request #57 from SolidLabResearch/master
Deploy
2 parents 9f8bddf + 29cb4b4 commit 1ce3457

32 files changed

Lines changed: 9694 additions & 2155 deletions

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ insert_final_newline = true
1616
charset = utf-8
1717
trim_trailing_whitespace = true
1818
indent_style = space
19-
indent_size = 2
19+
indent_size = 4
2020
max_line_length = 120

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@
44

55
# Solid Watchparty
66

7+
[solidlabresearch.github.io/solid-watch-party/](https://solidlabresearch.github.io/solid-watch-party/)
8+
79
Welcome to Solid Watchparty, a platform designed for shared media viewing experiences with a focus on decentralized data management using SOLID Pods. This project leverages the latest web technologies to offer a responsive and user-centric interface.
810

11+
12+
13+
https://github.com/SolidLabResearch/solid-watch-party/assets/37975937/e78ec297-3538-44c7-ad32-14ea310a35e7
14+
15+
16+
917
## :zap: Quick Start
1018

1119

@@ -14,6 +22,10 @@ Welcome to Solid Watchparty, a platform designed for shared media viewing experi
1422
```
1523
git clone git@github.com:SolidLabResearch/solid-watch-party.git
1624
```
25+
2. **Navigate to directory**
26+
```
27+
cd solid-watch-party/solid-watchparty/
28+
```
1729
2. **Install Dependencies**
1830
```
1931
npm install
@@ -36,7 +48,8 @@ This project is in active development. This in an overview of the progress:
3648
- [x] Creating new rooms and joining rooms
3749
- [x] Sending, retrieving and displaying messages
3850
- [x] Watching a video stream
39-
- [ ] Video stream syncing and controls
51+
- [x] Video stream syncing and controls
52+
- [ ] Privacy
4053
- [ ] Polishing
4154
2. **Statistics dashboard:**
4255
- *details to be determined*

solid-watchparty/package-lock.json

Lines changed: 7831 additions & 1388 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

solid-watchparty/package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,26 @@
1111
"preview": "vite preview"
1212
},
1313
"dependencies": {
14+
"@comunica/query-sparql-link-traversal": "^0.3.0",
15+
"@comunica/query-sparql-solid": "^2.4.0",
1416
"@incremunica/query-sparql-incremental": "^1.2.0",
1517
"@inrupt/solid-client": "^1.30.2",
1618
"@inrupt/solid-client-authn-browser": "^1.17.5",
1719
"@inrupt/solid-ui-react": "^2.9.0",
1820
"@inrupt/vocab-common-rdf": "^1.0.5",
21+
"class-variance-authority": "^0.7.0",
22+
"clsx": "^2.1.0",
1923
"dashjs": "^4.7.3",
24+
"lucide-react": "^0.363.0",
2025
"react": "^18.2.0",
2126
"react-dom": "^18.2.0",
22-
"react-router-dom": "^6.18.0"
27+
"react-full-screen": "^1.1.1",
28+
"react-icons": "^5.0.1",
29+
"react-player": "^2.15.1",
30+
"react-router-dom": "^6.18.0",
31+
"tailwind-merge": "^2.2.2",
32+
"tailwindcss-animate": "^1.0.7",
33+
"usehooks-ts": "^3.0.2"
2334
},
2435
"devDependencies": {
2536
"@types/react": "^18.2.15",

solid-watchparty/src/App.jsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
/* library imports */
2+
import { useState } from 'react';
23
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
34
import { SessionProvider } from '@inrupt/solid-ui-react'
45

6+
/* page imports */
57
import LoginPage from './pages/LoginPage';
68
import MenuPage from './pages/MenuPage';
79
import WatchPage from './pages/WatchPage';
810

11+
/* context imports */
12+
import { MessageBoxContext } from './contexts';
13+
914
/* config imports */
1015
import config from '../config';
1116

12-
export const router = createBrowserRouter([
13-
{path: (config.baseDir + "/"), element: <LoginPage/>},
14-
{path: (config.baseDir + "/menu"), element: <MenuPage/>},
15-
{path: (config.baseDir + "/watch"), element: <WatchPage/>},
17+
const router = createBrowserRouter([
18+
{path: (config.baseDir + "/"), element: <LoginPage/>},
19+
{path: (config.baseDir + "/menu"), element: <MenuPage/>},
20+
{path: (config.baseDir + "/watch"), element: <WatchPage/>},
1621
]);
1722

1823
function App() {
19-
return (
20-
<SessionProvider>
21-
<RouterProvider router={router}/>
22-
</SessionProvider>
23-
);
24+
const messageBox = useState(null);
25+
return (
26+
<SessionProvider>
27+
<MessageBoxContext.Provider value={messageBox}>
28+
<RouterProvider router={router}/>
29+
</MessageBoxContext.Provider>
30+
</SessionProvider>
31+
);
2432
}
2533

2634
export default App;
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/* library imports */
2+
import { useEffect, useState, useContext } from 'react';
3+
import { useSession, } from "@inrupt/solid-ui-react";
4+
import { FaUserCircle, FaCheck } from "react-icons/fa";
5+
import propTypes from 'prop-types';
6+
7+
/* component imports */
8+
import SWModal from '../components/SWModal';
9+
import SWLoadingIcon from '../components/SWLoadingIcon';
10+
import { MenuBar, MenuItem } from '../components/SWMenu';
11+
import SWSwitch from '../components/SWSwitch';
12+
13+
/* context imports */
14+
import { MessageBoxContext } from '../contexts';
15+
16+
/* service imports */
17+
import RoomSolidService from '../services/room.solidservice.js';
18+
import MessageSolidService from '../services/message.solidservice.js';
19+
20+
/* util imports */
21+
import { inSession } from '../utils/solidUtils';
22+
23+
/* TODO(Elias): Add validations and error handling everywhere */
24+
25+
function LoadingCard() {
26+
return (
27+
<div className="flex w-full h-full items-center justify-center">
28+
<SWLoadingIcon className="w-8 h-8"/>
29+
</div>
30+
);
31+
}
32+
33+
function PersonCard({person}) {
34+
const [enabled, setEnabled] = useState(false);
35+
const [messageBoxUrl, setMessageBoxUrl] = useContext(MessageBoxContext);
36+
const [isLoading, setIsLoading] = useState(true);
37+
const sessionContext = useSession();
38+
const isMyCard = (person.webId === sessionContext.session.info.webId);
39+
40+
useEffect(() => {
41+
const fetch = async () => {
42+
setIsLoading(true);
43+
const accessModes = await MessageSolidService.checkAccess(sessionContext, messageBoxUrl, person.webId);
44+
if (accessModes.error) {
45+
return;
46+
}
47+
setEnabled(accessModes.read);
48+
setIsLoading(false);
49+
}
50+
fetch();
51+
}, []);
52+
53+
const onSwitch = async () => {
54+
setIsLoading(true);
55+
const result = await MessageSolidService.setAccess(sessionContext, messageBoxUrl, person.webId,
56+
{read: !enabled});
57+
setIsLoading(false);
58+
if (result.error) {
59+
return;
60+
}
61+
setEnabled(result.read);
62+
}
63+
64+
return (
65+
<div className="rgb-bg-1 sw-border flex justify-between p-4 items-center">
66+
<div className="flex gap-3">
67+
<FaUserCircle className="rgb-1 sw-fw-1 w-6 h-6"/>
68+
<p>{person.name}</p>
69+
</div>
70+
<div className="flex gap-3 items-center">
71+
<p className="rgb-1">Allow seeing my messages:</p>
72+
<SWSwitch enabled={enabled} onSwitch={onSwitch} disabled={isMyCard} isLoading={isLoading}/>
73+
</div>
74+
</div>
75+
);
76+
}
77+
PersonCard.propTypes = {
78+
person: propTypes.object.isRequired,
79+
}
80+
81+
function RequestingPersonCard({person, roomUrl, removeFromPeople}) {
82+
const sessionContext = useSession();
83+
const [isLoading, setIsLoading] = useState(false);
84+
85+
const onAccept = async (person) => {
86+
setIsLoading(true);
87+
const result = await RoomSolidService.addPerson(sessionContext, roomUrl, person.messageBoxUrl, person.webId);
88+
setIsLoading(false);
89+
if (result.error) {
90+
console.error(result.error);
91+
return;
92+
}
93+
removeFromPeople(person);
94+
}
95+
96+
return (
97+
<div className="rgb-bg-1 sw-border flex justify-between p-4 h-fit">
98+
<div className="flex gap-3 justify-between w-full items-center">
99+
<div className="flex gap-3">
100+
<FaUserCircle className="rgb-1 sw-fw-1 w-6 h-6"/>
101+
<p className="rgb-on">{person.name}</p>
102+
</div>
103+
{isLoading ? (
104+
<div className="p-1">
105+
<SWLoadingIcon className={`w-5`}/>
106+
</div>
107+
) : (
108+
<button onClick={() => onAccept(person)}
109+
className="p-2 rounded items-center rgb-bg-active-1 hover:rgb-bg-1
110+
active:rgb-bg-active-2 rgb-active-1 active:rgb-1">
111+
<FaCheck/>
112+
</button>
113+
)}
114+
</div>
115+
</div>
116+
);
117+
}
118+
RequestingPersonCard.propTypes = {
119+
person: propTypes.object.isRequired,
120+
roomUrl: propTypes.string.isRequired,
121+
removeFromPeople: propTypes.func.isRequired,
122+
}
123+
124+
function InRoomPeople({roomUrl}) {
125+
const sessionContext = useSession();
126+
const [isLoading, setIsLoading] = useState(true);
127+
const [people, setPeople] = useState([]);
128+
129+
useEffect(() => {
130+
const getPeople = async () => {
131+
let peopleResult = await RoomSolidService.getPeople(sessionContext, roomUrl);
132+
if (peopleResult.error) {
133+
console.error(peopleResult.error);
134+
return;
135+
}
136+
peopleResult = peopleResult.map((person, index) => ({...person, key: index}));
137+
setIsLoading(false);
138+
setPeople(peopleResult);
139+
};
140+
if (inSession(sessionContext)) {
141+
getPeople();
142+
}
143+
}, []);
144+
145+
if (isLoading) {
146+
return (<LoadingCard/>);
147+
}
148+
return (
149+
<div className="overflow-auto grid grid-cols-2 auto-rows-min gap-4 h-[90%]">
150+
{people.map((person, index) => <PersonCard person={person} key={index}/>)}
151+
</div>
152+
);
153+
}
154+
InRoomPeople.propTypes = {
155+
roomUrl: propTypes.string.isRequired,
156+
}
157+
158+
function RequestingPeople({roomUrl}) {
159+
const sessionContext = useSession();
160+
const [isLoading, setIsLoading] = useState(true);
161+
const [people, setPeople] = useState([]);
162+
163+
useEffect(() => {
164+
const getPeople = async () => {
165+
let peopleResult = await RoomSolidService.getActiveRegisterPeople(sessionContext, roomUrl);
166+
if (peopleResult.error) {
167+
console.error(peopleResult.error);
168+
return;
169+
}
170+
peopleResult = peopleResult.map((person, index) => ({...person, key: index}));
171+
setIsLoading(false);
172+
setPeople(peopleResult);
173+
};
174+
if (inSession(sessionContext)) {
175+
getPeople();
176+
}
177+
}, []);
178+
179+
const removeFromPeople = (person) => {
180+
setPeople(people.filter((p) => (p.webId !== person.webId)));
181+
}
182+
183+
if (isLoading) {
184+
return (<LoadingCard/>);
185+
}
186+
return (
187+
<div className="overflow-auto grid grid-cols-2 auto-rows-min gap-4 h-[90%]">
188+
{people.map((person, index) => (
189+
<RequestingPersonCard person={person} key={index} roomUrl={roomUrl}
190+
removeFromPeople={removeFromPeople}/>
191+
))}
192+
</div>
193+
);
194+
}
195+
RequestingPeople.propTypes = {
196+
roomUrl: propTypes.string.isRequired,
197+
}
198+
199+
function PeopleMenuModal({setModalIsShown, roomUrl}) {
200+
/* NOTE(Elias): Uses strings for pages, valid options are:
201+
* 1. in-room
202+
* 2. requesting */
203+
const [tab, setTab] = useState("in-room");
204+
205+
let body = <></>
206+
switch (tab) {
207+
case "in-room":
208+
body = <InRoomPeople roomUrl={roomUrl}/>;
209+
break;
210+
case "requesting":
211+
body = <RequestingPeople roomUrl={roomUrl}/>;
212+
break;
213+
}
214+
215+
return (
216+
<SWModal className="rgb-bg-2 h-2/3 p-12 z-10 w-1/2 sw-border" setIsShown={setModalIsShown}>
217+
<div className="mb-6 flex items-center justify-between">
218+
<p className="sw-fs-2 sw-fw-1">People</p>
219+
<MenuBar>
220+
<MenuItem onClick={() => setTab("in-room")}>In Room</MenuItem>
221+
<MenuItem onClick={() => setTab("requesting")}>Requesting</MenuItem>
222+
</MenuBar>
223+
</div>
224+
{body}
225+
</SWModal>
226+
);
227+
}
228+
PeopleMenuModal.propTypes = {
229+
setModalIsShown: propTypes.func.isRequired,
230+
roomUrl: propTypes.string.isRequired,
231+
}
232+
233+
export default PeopleMenuModal;

solid-watchparty/src/components/SWAutoScrollDiv.jsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,27 @@ import PropTypes from 'prop-types';
55

66
function AutoScrollDiv({children, className})
77
{
8-
const divRef = useRef(null);
9-
const [isUserAtBottom, setIsUserAtBottom] = useState(true);
8+
const divRef = useRef(null);
9+
const [isUserAtBottom, setIsUserAtBottom] = useState(true);
1010

11-
const onScroll = () => {
12-
const div = divRef.current;
13-
const atBottom = div.scrollHeight - div.scrollTop - div.clientHeight < 10;
14-
setIsUserAtBottom(atBottom);
15-
};
11+
const onScroll = () => {
12+
const div = divRef.current;
13+
const atBottom = div.scrollHeight - div.scrollTop - div.clientHeight < 10;
14+
setIsUserAtBottom(atBottom);
15+
};
1616

17-
useEffect(() => {
18-
const div = divRef.current;
19-
if (isUserAtBottom) {
20-
div.scrollTop = div.scrollHeight;
21-
}
22-
}, [children, isUserAtBottom]);
17+
useEffect(() => {
18+
const div = divRef.current;
19+
if (isUserAtBottom) {
20+
div.scrollTop = div.scrollHeight;
21+
}
22+
}, [children, isUserAtBottom]);
2323

24-
return (
25-
<div ref={divRef} onScroll={onScroll} className={'' + className}>
26-
{children}
27-
</div>
28-
);
24+
return (
25+
<div ref={divRef} onScroll={onScroll} className={'' + className}>
26+
{children}
27+
</div>
28+
);
2929
}
3030

3131
AutoScrollDiv.propTypes = {

0 commit comments

Comments
 (0)