diff --git a/src/App.css b/src/App.css index b9b9c1f..aaad548 100644 --- a/src/App.css +++ b/src/App.css @@ -1,8 +1,5 @@ body { --ion-color-primary: #107c76; - --ion-background-color: rgb(39, 39, 39); - --ion-text-color: white; - --ion-text-color-rgb: 255, 255, 255; } .modal-wrapper { @@ -12,8 +9,6 @@ body { .App { text-align: center; height: 100vh; - background-color: #282c34; - color: white; } .app-title-container { @@ -65,8 +60,6 @@ body { align-items: center; justify-content: center; font-size: calc(10px + 2vmin); - color: white; - border-bottom: 1px solid white; } .App-link { diff --git a/src/App.tsx b/src/App.tsx index 2c4b0ca..4ef8a4a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { WSClient } from "./WSClient"; import { ServerTypes } from "./ServerTypes"; import { SessionPage } from "./feature/session/SessionPage"; import { NavigateOnStateChange } from "./NavigateOnStateChange"; +import { SessionMessage, mapSessionMsg } from "./feature/session/SessionMessage"; export const App: React.FC = () => { let wsUrl = "wss://qrsync.org/api/v1/ws"; @@ -19,6 +20,8 @@ export const App: React.FC = () => { const [clientMap, setClientMap] = useState< Record >({}); + const [sessionMessages, setSessionMessages] = useState([]); + // State used to navigate to route via a server sent event (not user link click) const [changeToRoute, setChangeToRoute] = useState(); @@ -73,8 +76,12 @@ export const App: React.FC = () => { }; const onBroadcastFromSessionMsg = ( - msg: ServerTypes.BroadcastFromSessionMsg - ) => {}; + serverMsg: ServerTypes.BroadcastFromSessionMsg + ) => { + const newMsg = mapSessionMsg(serverMsg); + const newMsgs = sessionMessages.concat([newMsg]); + setSessionMessages(newMsgs); + }; const onLeaveSession = () => { wsClient.leaveSession(); @@ -82,6 +89,19 @@ export const App: React.FC = () => { setChangeToRoute("/index.html"); }; + const onShare = (msg: SessionMessage) => { + let msgWithSender: SessionMessage = { + ...msg, + senderId: wsClient.getId() ?? "", + senderName: wsClient.getName(), + } + let serverMsg: ServerTypes.BroadcastToSessionMsg = { + type: "BroadcastToSession", + payload: msgWithSender + }; + wsClient.sendMessage(serverMsg); + } + return ( @@ -99,10 +119,12 @@ export const App: React.FC = () => { diff --git a/src/feature/clients/ClientsCard.tsx b/src/feature/clients/ClientsCard.tsx new file mode 100644 index 0000000..719f472 --- /dev/null +++ b/src/feature/clients/ClientsCard.tsx @@ -0,0 +1,66 @@ +import { + IonCard, + IonCardHeader, + IonCardTitle, + IonCardSubtitle, + IonCardContent, + IonButton, +} from "@ionic/react"; +import React from "react"; +import { ServerTypes } from "../../ServerTypes"; + +export type ClientsCardProps = { + ownerId: string | undefined; + clientMap: Record; + addClientClicked: () => void; +}; + +export const ClientsCard: React.FC = ({ + clientMap, + ownerId, + addClientClicked, +}) => { + const allClients = Object.values(clientMap); + const onlyOwner = ownerId && clientMap[ownerId] && allClients.length <= 1; + + return ( + + + Clients + Devices connected to this session + + +
+ {onlyOwner ? ( +

You are the only client connected to this session.

+ ) : null} + {allClients.map((client) => ( + + ))} + Add Client +
+
+
+ ); +}; + +type ClientCircleProps = { + client: ServerTypes.Client; +}; + +const ClientCircle: React.FC = ({ client }) => { + return ( +
+ {client.id} : {client.name} +
+ ); +}; diff --git a/src/feature/history/HistoryCard.tsx b/src/feature/history/HistoryCard.tsx new file mode 100644 index 0000000..e1bd129 --- /dev/null +++ b/src/feature/history/HistoryCard.tsx @@ -0,0 +1,34 @@ +import { IonCard, IonCardHeader, IonCardTitle, IonCardContent, IonItem } from "@ionic/react"; +import React from "react"; +import { SessionMessage } from "../session/SessionMessage"; + +export type HistoryCardProps = { + messages: SessionMessage[]; +} + +export const HistoryCard: React.FC = ({messages}) => { + return + + History + + + { messages.length === 0 ? : + messages.map(msg => ) } + + ; +}; + +type MessageProps = { + msg: SessionMessage; +} + +const Message: React.FC = ({msg}) => { + return + {msg.senderName}: + {msg.text} + +} + +const NoMessages: React.FC = () => { + return
Shared content will show up here.
+} \ No newline at end of file diff --git a/src/feature/home/HomePage.tsx b/src/feature/home/HomePage.tsx index 90cb2b2..a39ed49 100644 --- a/src/feature/home/HomePage.tsx +++ b/src/feature/home/HomePage.tsx @@ -68,7 +68,7 @@ export const HomePage: React.FC = ({ { - state = { - urlInput: "", - }; - - onOpenWebsiteClick = () => { - let id = this.props.wsClient.getId(); - if (this.props.wsClient && id != null) { - let sessionMsg: SessionMessage = { - type: "OPEN_WEBSITE", - text: this.state.urlInput, - senderId: id, - senderName: this.props.wsClient.getName(), - }; - this.props.wsClient.sendMessage({ - type: "BroadcastToSession", - payload: sessionMsg, - }); - } - this.props.closeModal(); - }; - - render() { - return ( - <> -

Open Website

-

Open a website on devices controlled by this session

- this.setState({ urlInput: e.detail.value! })} - > - Open Website - - ); - } -} diff --git a/src/feature/session-actions/SessionActionModal.ts b/src/feature/session-actions/SessionActionModal.ts deleted file mode 100644 index 8f0b7a6..0000000 --- a/src/feature/session-actions/SessionActionModal.ts +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; -import { WSClient } from "../../WSClient"; - -export interface SessionActionModalProps { - wsClient: WSClient; - closeModal: () => any; -} - -export class SessionActionModal

extends React.Component { - -} \ No newline at end of file diff --git a/src/feature/session-actions/SessionActionsList.tsx b/src/feature/session-actions/SessionActionsList.tsx deleted file mode 100644 index 7ce4683..0000000 --- a/src/feature/session-actions/SessionActionsList.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React from "react"; -import { globeOutline } from "ionicons/icons"; -import { IonIcon, IonModal } from "@ionic/react"; -import { OpenWebsiteModal } from "./OpenWebsiteModal"; -import { WSClient } from "../../WSClient"; -import { SessionActionModal } from "./SessionActionModal"; - -export interface SessionActionsListProps { - wsClient: WSClient; - userIsSessionOwner: boolean; -} - -export interface SessionActionsListState { - actionModalOpen: boolean; - actionModalComponent?: typeof SessionActionModal; -} - -export interface SessionActionSummary { - name: string; - ionicon: string; - ownerOnly: boolean; - modalComponent?: typeof SessionActionModal; -} - -const sessionActions: SessionActionSummary[] = [ - { - name: "Open Website", - ionicon: globeOutline, - ownerOnly: true, - modalComponent: OpenWebsiteModal, - }, -]; - -export class SessionActionsList extends React.Component< - SessionActionsListProps, - SessionActionsListState -> { - state: SessionActionsListState = { - actionModalOpen: false, - }; - - render() { - console.log("User is session owner", this.props.userIsSessionOwner); - return ( -

-

- User is session owner? {this.props.userIsSessionOwner ? "yes" : "no"} -

- {sessionActions - .filter( - (action) => this.props.userIsSessionOwner || !action.ownerOnly - ) - .map((action) => this.renderActionButton(action))} - - {this.renderModal()} - -
- ); - } - - renderActionButton = (action: SessionActionSummary) => { - return ( -
this.onActionClicked(action)} - style={{ - borderWidth: 1, - borderStyle: "solid", - borderColor: "white", - borderRadius: 20, - display: "inline-block", - }} - > -

{action.name}

- -
- ); - }; - - renderModal = () => { - let ModalComponent = this.state.actionModalComponent; - if (ModalComponent) { - return ( - - ); - } - }; - - closeModal = () => { - this.setState({ actionModalOpen: false }); - }; - - onActionClicked = (action: SessionActionSummary) => { - if (action.name === "Open Website") { - this.setState({ - actionModalOpen: true, - actionModalComponent: action.modalComponent, - }); - } - }; -} diff --git a/src/feature/session/SessionMessage.ts b/src/feature/session/SessionMessage.ts index 6394390..1d953fb 100644 --- a/src/feature/session/SessionMessage.ts +++ b/src/feature/session/SessionMessage.ts @@ -1,12 +1,30 @@ -export type SessionMessageType = "TEXT_MESSAGE" | "FILE" | "OPEN_WEBSITE"; +import { ServerTypes } from "../../ServerTypes"; + +export const sessionMessageTypes = ["TEXT_MESSAGE", "FILE", "OPEN_WEBSITE"] as const; +export type SessionMessageType = typeof sessionMessageTypes[number]; export interface SessionMessage { - type: SessionMessageType; - senderId: string; - senderName: string; - text: string; + uuid?: string; + type?: SessionMessageType; + senderId?: string; + senderName?: string; + text?: string; } export interface OpenWebsiteSessionMessage extends SessionMessage { type: "OPEN_WEBSITE" +} + +export function mapSessionMsg(serverMsg: ServerTypes.BroadcastFromSessionMsg): SessionMessage { + let sessionMsgType = serverMsg.payload["type"] as SessionMessageType | undefined; + if (sessionMessageTypes.indexOf(sessionMsgType as SessionMessageType) < 0) { + sessionMsgType = undefined; + } + return { + uuid: ("" + Math.random() + "-" + Math.random()).replace(".", "0"), + type: sessionMsgType, + senderId: serverMsg.senderId, + senderName: serverMsg.payload["senderName"] ?? "", + text: serverMsg.payload["text"] ?? "" + }; } \ No newline at end of file diff --git a/src/feature/session/SessionPage.tsx b/src/feature/session/SessionPage.tsx index 500197a..f43db1b 100644 --- a/src/feature/session/SessionPage.tsx +++ b/src/feature/session/SessionPage.tsx @@ -1,77 +1,58 @@ import React from "react"; import { ServerTypes } from "../../ServerTypes"; -import { IonButton, IonPage } from "@ionic/react"; -import { WSClient } from "../../WSClient"; +import { + IonButton, + IonContent, + IonHeader, + IonPage, + IonTitle, + IonToolbar, +} from "@ionic/react"; +import { ClientsCard } from "../clients/ClientsCard"; +import { HistoryCard } from "../history/HistoryCard"; +import { ShareCard } from "../share/ShareCard"; import { SessionMessage } from "./SessionMessage"; -import { SessionActionsList } from "../session-actions/SessionActionsList"; export interface SessionPageProps { + sessionMessages: SessionMessage[]; sessionId: string | undefined; sessionOwnerId: string | undefined; clientMap: Record; - wsClient: WSClient; + userIsSessionOwner: boolean; + onShare: (msg: SessionMessage) => void; onLeaveSession: () => any; } -export interface SessionPageState { - urlInput: string; - sessionMessages: SessionMessage[]; -} - -export class SessionPage extends React.Component< - SessionPageProps, - SessionPageState -> { - state = { - urlInput: "", - sessionMessages: [], - } as SessionPageState; - - constructor(props: SessionPageProps) { - super(props); - props.wsClient.addMessageHandler("session_page", this.onWebsocketMessage); - } - - render() { - return ( - - Leave Session -

Session Page

-

{this.props.sessionId}

-

Session Messages

-
    - {this.state.sessionMessages.map((sessionMsg) => ( -
  • {sessionMsg.text}
  • - ))} -
-

Session Actions:

- -
- ); - } - - onWebsocketMessage = (msg: ServerTypes.Msg) => { - if (msg.type === "BroadcastFromSession") { - let payload: SessionMessage = msg.payload; - if (payload.type && payload.text) { - let sessionMsgs: SessionMessage[] = []; - if (this.state.sessionMessages) { - sessionMsgs = this.state.sessionMessages; - } - sessionMsgs.push(payload); - this.setState({ sessionMessages: sessionMsgs }); - } - if (msg.senderId !== this.props.wsClient.getId()) { - if (payload.type === "OPEN_WEBSITE") { - console.log("Opening url ", payload.text); - window.open(payload.text, "_blank"); - } - } - } - }; -} +export const SessionPage: React.FC = ({ + sessionMessages, + sessionId, + sessionOwnerId, + clientMap, + userIsSessionOwner, + onShare, + onLeaveSession, +}) => { + return ( + + + + QR Sync - Session + + + + { + /* TODO: Implement opening add client modal */ + }} + /> + + + Leave Session with id: {sessionId} + + + ); +}; diff --git a/src/feature/share/ShareCard.tsx b/src/feature/share/ShareCard.tsx new file mode 100644 index 0000000..7233598 --- /dev/null +++ b/src/feature/share/ShareCard.tsx @@ -0,0 +1,32 @@ +import { IonCard, IonCardHeader, IonCardTitle, IonCardContent, IonButton, IonModal } from "@ionic/react"; +import React, { useState } from "react"; +import { SessionMessage, SessionMessageType } from "../session/SessionMessage"; +import { ShareModal } from "./share-actions/ShareModal"; + +export type ShareCardProps = { + onShare: (msg: SessionMessage) => void; + userIsSessionOwner?: boolean; +} + +export const ShareCard: React.FC = ({onShare, userIsSessionOwner}) => { + const [shareModalTypeOpen, setShareModalTypeOpen] = useState(null); + + return + + Share + + + {/* Send text message */} + setShareModalTypeOpen("TEXT_MESSAGE")} + >Send Message + + setShareModalTypeOpen(null)}> + setShareModalTypeOpen(null)} + /> + + ; +}; \ No newline at end of file diff --git a/src/feature/share/share-actions/SendMessageModal.tsx b/src/feature/share/share-actions/SendMessageModal.tsx new file mode 100644 index 0000000..a45853d --- /dev/null +++ b/src/feature/share/share-actions/SendMessageModal.tsx @@ -0,0 +1,34 @@ +import { IonButton, IonInput } from "@ionic/react"; +import React, { useState } from "react"; +import { ShareModalProps } from "./ShareModal"; +import { SessionMessage } from "../../session/SessionMessage"; + +export const SendMessageModal: React.FC = ({ + onShare, + closeModal, +}) => { + + const [text, setText] = useState(""); + + const sendClick = () => { + let sessionMsg: SessionMessage = { + type: "TEXT_MESSAGE", + text: text, + }; + onShare(sessionMsg); + closeModal(); + }; + + return ( +
+

Send message

+

Send message to all clients in this session.

+ setText(e.detail.value!)} + > + sendClick()}>Send +
+ ); +} diff --git a/src/feature/share/share-actions/ShareModal.tsx b/src/feature/share/share-actions/ShareModal.tsx new file mode 100644 index 0000000..db6b1ca --- /dev/null +++ b/src/feature/share/share-actions/ShareModal.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { SessionMessage, SessionMessageType } from "../../session/SessionMessage"; +import { SendMessageModal } from "./SendMessageModal"; + +export type ShareModalProps = { + type: SessionMessageType | null; + onShare: (msg: SessionMessage) => void; + closeModal: () => any; +}; + +export const ShareModal: React.FC = ({type, onShare, closeModal}) => { + switch(type) { + case "TEXT_MESSAGE": return + default: return null; + } +} \ No newline at end of file