Skip to content

Commit

Permalink
Merge pull request #57 from SolidLabResearch/master
Browse files Browse the repository at this point in the history
Deploy
  • Loading branch information
eliasnijs authored Apr 9, 2024
2 parents 9f8bddf + 29cb4b4 commit 1ce3457
Show file tree
Hide file tree
Showing 32 changed files with 9,694 additions and 2,155 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
indent_size = 4
max_line_length = 120
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@

# Solid Watchparty

[solidlabresearch.github.io/solid-watch-party/](https://solidlabresearch.github.io/solid-watch-party/)

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.



https://github.com/SolidLabResearch/solid-watch-party/assets/37975937/e78ec297-3538-44c7-ad32-14ea310a35e7



## :zap: Quick Start


Expand All @@ -14,6 +22,10 @@ Welcome to Solid Watchparty, a platform designed for shared media viewing experi
```
git clone [email protected]:SolidLabResearch/solid-watch-party.git
```
2. **Navigate to directory**
```
cd solid-watch-party/solid-watchparty/
```
2. **Install Dependencies**
```
npm install
Expand All @@ -36,7 +48,8 @@ This project is in active development. This in an overview of the progress:
- [x] Creating new rooms and joining rooms
- [x] Sending, retrieving and displaying messages
- [x] Watching a video stream
- [ ] Video stream syncing and controls
- [x] Video stream syncing and controls
- [ ] Privacy
- [ ] Polishing
2. **Statistics dashboard:**
- *details to be determined*
Expand Down
9,219 changes: 7,831 additions & 1,388 deletions solid-watchparty/package-lock.json

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion solid-watchparty/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,26 @@
"preview": "vite preview"
},
"dependencies": {
"@comunica/query-sparql-link-traversal": "^0.3.0",
"@comunica/query-sparql-solid": "^2.4.0",
"@incremunica/query-sparql-incremental": "^1.2.0",
"@inrupt/solid-client": "^1.30.2",
"@inrupt/solid-client-authn-browser": "^1.17.5",
"@inrupt/solid-ui-react": "^2.9.0",
"@inrupt/vocab-common-rdf": "^1.0.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"dashjs": "^4.7.3",
"lucide-react": "^0.363.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.18.0"
"react-full-screen": "^1.1.1",
"react-icons": "^5.0.1",
"react-player": "^2.15.1",
"react-router-dom": "^6.18.0",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.0.2"
},
"devDependencies": {
"@types/react": "^18.2.15",
Expand Down
26 changes: 17 additions & 9 deletions solid-watchparty/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
/* library imports */
import { useState } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { SessionProvider } from '@inrupt/solid-ui-react'

/* page imports */
import LoginPage from './pages/LoginPage';
import MenuPage from './pages/MenuPage';
import WatchPage from './pages/WatchPage';

/* context imports */
import { MessageBoxContext } from './contexts';

/* config imports */
import config from '../config';

export const router = createBrowserRouter([
{path: (config.baseDir + "/"), element: <LoginPage/>},
{path: (config.baseDir + "/menu"), element: <MenuPage/>},
{path: (config.baseDir + "/watch"), element: <WatchPage/>},
const router = createBrowserRouter([
{path: (config.baseDir + "/"), element: <LoginPage/>},
{path: (config.baseDir + "/menu"), element: <MenuPage/>},
{path: (config.baseDir + "/watch"), element: <WatchPage/>},
]);

function App() {
return (
<SessionProvider>
<RouterProvider router={router}/>
</SessionProvider>
);
const messageBox = useState(null);
return (
<SessionProvider>
<MessageBoxContext.Provider value={messageBox}>
<RouterProvider router={router}/>
</MessageBoxContext.Provider>
</SessionProvider>
);
}

export default App;
Expand Down
233 changes: 233 additions & 0 deletions solid-watchparty/src/components/PeopleMenuModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/* library imports */
import { useEffect, useState, useContext } from 'react';
import { useSession, } from "@inrupt/solid-ui-react";
import { FaUserCircle, FaCheck } from "react-icons/fa";
import propTypes from 'prop-types';

/* component imports */
import SWModal from '../components/SWModal';
import SWLoadingIcon from '../components/SWLoadingIcon';
import { MenuBar, MenuItem } from '../components/SWMenu';
import SWSwitch from '../components/SWSwitch';

/* context imports */
import { MessageBoxContext } from '../contexts';

/* service imports */
import RoomSolidService from '../services/room.solidservice.js';
import MessageSolidService from '../services/message.solidservice.js';

/* util imports */
import { inSession } from '../utils/solidUtils';

/* TODO(Elias): Add validations and error handling everywhere */

function LoadingCard() {
return (
<div className="flex w-full h-full items-center justify-center">
<SWLoadingIcon className="w-8 h-8"/>
</div>
);
}

function PersonCard({person}) {
const [enabled, setEnabled] = useState(false);
const [messageBoxUrl, setMessageBoxUrl] = useContext(MessageBoxContext);
const [isLoading, setIsLoading] = useState(true);
const sessionContext = useSession();
const isMyCard = (person.webId === sessionContext.session.info.webId);

useEffect(() => {
const fetch = async () => {
setIsLoading(true);
const accessModes = await MessageSolidService.checkAccess(sessionContext, messageBoxUrl, person.webId);
if (accessModes.error) {
return;
}
setEnabled(accessModes.read);
setIsLoading(false);
}
fetch();
}, []);

const onSwitch = async () => {
setIsLoading(true);
const result = await MessageSolidService.setAccess(sessionContext, messageBoxUrl, person.webId,
{read: !enabled});
setIsLoading(false);
if (result.error) {
return;
}
setEnabled(result.read);
}

return (
<div className="rgb-bg-1 sw-border flex justify-between p-4 items-center">
<div className="flex gap-3">
<FaUserCircle className="rgb-1 sw-fw-1 w-6 h-6"/>
<p>{person.name}</p>
</div>
<div className="flex gap-3 items-center">
<p className="rgb-1">Allow seeing my messages:</p>
<SWSwitch enabled={enabled} onSwitch={onSwitch} disabled={isMyCard} isLoading={isLoading}/>
</div>
</div>
);
}
PersonCard.propTypes = {
person: propTypes.object.isRequired,
}

function RequestingPersonCard({person, roomUrl, removeFromPeople}) {
const sessionContext = useSession();
const [isLoading, setIsLoading] = useState(false);

const onAccept = async (person) => {
setIsLoading(true);
const result = await RoomSolidService.addPerson(sessionContext, roomUrl, person.messageBoxUrl, person.webId);
setIsLoading(false);
if (result.error) {
console.error(result.error);
return;
}
removeFromPeople(person);
}

return (
<div className="rgb-bg-1 sw-border flex justify-between p-4 h-fit">
<div className="flex gap-3 justify-between w-full items-center">
<div className="flex gap-3">
<FaUserCircle className="rgb-1 sw-fw-1 w-6 h-6"/>
<p className="rgb-on">{person.name}</p>
</div>
{isLoading ? (
<div className="p-1">
<SWLoadingIcon className={`w-5`}/>
</div>
) : (
<button onClick={() => onAccept(person)}
className="p-2 rounded items-center rgb-bg-active-1 hover:rgb-bg-1
active:rgb-bg-active-2 rgb-active-1 active:rgb-1">
<FaCheck/>
</button>
)}
</div>
</div>
);
}
RequestingPersonCard.propTypes = {
person: propTypes.object.isRequired,
roomUrl: propTypes.string.isRequired,
removeFromPeople: propTypes.func.isRequired,
}

function InRoomPeople({roomUrl}) {
const sessionContext = useSession();
const [isLoading, setIsLoading] = useState(true);
const [people, setPeople] = useState([]);

useEffect(() => {
const getPeople = async () => {
let peopleResult = await RoomSolidService.getPeople(sessionContext, roomUrl);
if (peopleResult.error) {
console.error(peopleResult.error);
return;
}
peopleResult = peopleResult.map((person, index) => ({...person, key: index}));
setIsLoading(false);
setPeople(peopleResult);
};
if (inSession(sessionContext)) {
getPeople();
}
}, []);

if (isLoading) {
return (<LoadingCard/>);
}
return (
<div className="overflow-auto grid grid-cols-2 auto-rows-min gap-4 h-[90%]">
{people.map((person, index) => <PersonCard person={person} key={index}/>)}
</div>
);
}
InRoomPeople.propTypes = {
roomUrl: propTypes.string.isRequired,
}

function RequestingPeople({roomUrl}) {
const sessionContext = useSession();
const [isLoading, setIsLoading] = useState(true);
const [people, setPeople] = useState([]);

useEffect(() => {
const getPeople = async () => {
let peopleResult = await RoomSolidService.getActiveRegisterPeople(sessionContext, roomUrl);
if (peopleResult.error) {
console.error(peopleResult.error);
return;
}
peopleResult = peopleResult.map((person, index) => ({...person, key: index}));
setIsLoading(false);
setPeople(peopleResult);
};
if (inSession(sessionContext)) {
getPeople();
}
}, []);

const removeFromPeople = (person) => {
setPeople(people.filter((p) => (p.webId !== person.webId)));
}

if (isLoading) {
return (<LoadingCard/>);
}
return (
<div className="overflow-auto grid grid-cols-2 auto-rows-min gap-4 h-[90%]">
{people.map((person, index) => (
<RequestingPersonCard person={person} key={index} roomUrl={roomUrl}
removeFromPeople={removeFromPeople}/>
))}
</div>
);
}
RequestingPeople.propTypes = {
roomUrl: propTypes.string.isRequired,
}

function PeopleMenuModal({setModalIsShown, roomUrl}) {
/* NOTE(Elias): Uses strings for pages, valid options are:
* 1. in-room
* 2. requesting */
const [tab, setTab] = useState("in-room");

let body = <></>
switch (tab) {
case "in-room":
body = <InRoomPeople roomUrl={roomUrl}/>;
break;
case "requesting":
body = <RequestingPeople roomUrl={roomUrl}/>;
break;
}

return (
<SWModal className="rgb-bg-2 h-2/3 p-12 z-10 w-1/2 sw-border" setIsShown={setModalIsShown}>
<div className="mb-6 flex items-center justify-between">
<p className="sw-fs-2 sw-fw-1">People</p>
<MenuBar>
<MenuItem onClick={() => setTab("in-room")}>In Room</MenuItem>
<MenuItem onClick={() => setTab("requesting")}>Requesting</MenuItem>
</MenuBar>
</div>
{body}
</SWModal>
);
}
PeopleMenuModal.propTypes = {
setModalIsShown: propTypes.func.isRequired,
roomUrl: propTypes.string.isRequired,
}

export default PeopleMenuModal;
36 changes: 18 additions & 18 deletions solid-watchparty/src/components/SWAutoScrollDiv.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,27 @@ import PropTypes from 'prop-types';

function AutoScrollDiv({children, className})
{
const divRef = useRef(null);
const [isUserAtBottom, setIsUserAtBottom] = useState(true);
const divRef = useRef(null);
const [isUserAtBottom, setIsUserAtBottom] = useState(true);

const onScroll = () => {
const div = divRef.current;
const atBottom = div.scrollHeight - div.scrollTop - div.clientHeight < 10;
setIsUserAtBottom(atBottom);
};
const onScroll = () => {
const div = divRef.current;
const atBottom = div.scrollHeight - div.scrollTop - div.clientHeight < 10;
setIsUserAtBottom(atBottom);
};

useEffect(() => {
const div = divRef.current;
if (isUserAtBottom) {
div.scrollTop = div.scrollHeight;
}
}, [children, isUserAtBottom]);
useEffect(() => {
const div = divRef.current;
if (isUserAtBottom) {
div.scrollTop = div.scrollHeight;
}
}, [children, isUserAtBottom]);

return (
<div ref={divRef} onScroll={onScroll} className={'' + className}>
{children}
</div>
);
return (
<div ref={divRef} onScroll={onScroll} className={'' + className}>
{children}
</div>
);
}

AutoScrollDiv.propTypes = {
Expand Down
Loading

0 comments on commit 1ce3457

Please sign in to comment.