From 7c3b0a3ab01fa9367eac94966b0700039d20c457 Mon Sep 17 00:00:00 2001 From: Carter Date: Tue, 3 Dec 2024 09:33:43 -0600 Subject: [PATCH] team chat is now functional --- src/app/HeaderButtons.tsx | 56 ++++++- src/chat/Chat.tsx | 125 ++++++++++++++ src/chat/ChatInput.tsx | 205 +++++++++++++++++++++++ src/chat/ChatMessage.tsx | 288 +++++++++++++++++++++++++++++++++ src/chat/ChatOutput.tsx | 86 ++++++++++ src/models/Note.ts | 45 ++++++ src/models/index.ts | 2 +- src/pages/Team.tsx | 81 +++++++++- src/providers/index.ts | 2 +- src/providers/pond/noteAPI.tsx | 89 ++++++++++ src/providers/pond/pond.tsx | 9 +- src/providers/pond/userAPI.tsx | 48 +++--- src/user/UserMenu.tsx | 9 +- 13 files changed, 1004 insertions(+), 41 deletions(-) create mode 100644 src/chat/Chat.tsx create mode 100644 src/chat/ChatInput.tsx create mode 100644 src/chat/ChatMessage.tsx create mode 100644 src/chat/ChatOutput.tsx create mode 100644 src/models/Note.ts create mode 100644 src/providers/pond/noteAPI.tsx diff --git a/src/app/HeaderButtons.tsx b/src/app/HeaderButtons.tsx index a6b1fd1..1b349e0 100644 --- a/src/app/HeaderButtons.tsx +++ b/src/app/HeaderButtons.tsx @@ -1,13 +1,17 @@ -import { Chat } from "@mui/icons-material"; -import { Box, IconButton, Theme, Tooltip, useTheme } from "@mui/material"; +import { Chat as ChatIcon, ChevronRight } from "@mui/icons-material"; +import { Box, Drawer, IconButton, Theme, Tooltip, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; +import Chat from "chat/Chat"; +import { useMobile } from "hooks"; import { Team, User } from "models"; import moment from "moment"; +import { pond } from "protobuf-ts/pond"; import { useEffect, useState } from "react"; -import { getSignatureAccentColour, hasTransparentLogoBG } from "services/whiteLabel"; +import { getSignatureAccentColour } from "services/whiteLabel"; const useStyles = makeStyles((theme: Theme) => ({ button: { + color: getSignatureAccentColour(), width: theme.spacing(6), height: theme.spacing(6), marginTop: "auto", @@ -28,6 +32,25 @@ const useStyles = makeStyles((theme: Theme) => ({ off: { backgroundColor: "transparent" }, + chatDrawer: { + padding: theme.spacing(1), + height: "100%", + minWidth: theme.spacing(54), + maxWidth: theme.spacing(54) + }, + chatDrawerMobile: { + padding: theme.spacing(1), + height: "auto", + width: "100%" + }, + drawerHeader: { + display: "flex", + alignItems: "center", + padding: theme.spacing(0, 1), + // necessary for content to be below app bar + ...theme.mixins.toolbar, + justifyContent: "flex-start" + } })) interface Props { @@ -37,12 +60,13 @@ interface Props { } export default function HeaderButtons(props: Props) { - const {hasTeams, user, team} = props + const {hasTeams, /*user,*/ team} = props const [hasNewChats, setHasNewChats] = useState(false); const [chatDrawerOpen, setChatDrawerOpen] = useState(false); // const theme = useTheme(); const classes = useStyles(); + const isMobile = useMobile(); useEffect(() => { if (!team) return; @@ -74,7 +98,7 @@ export default function HeaderButtons(props: Props) { setChatDrawerOpen(true); setHasNewChats(false); }}> - + @@ -92,7 +116,27 @@ export default function HeaderButtons(props: Props) { } return ( - chatButton() + <> + {chatButton()} + {team && team.settings && team.settings.key && ( + setChatDrawerOpen(false)} + style={{ height: "100%", maxWidth: "100%" }} + title={team.settings.name ? team.settings.name + "Chat" : "Chat"}> +
+
+ setChatDrawerOpen(false)}> + + + {team.name()} Chat +
+ +
+
+ )} + ) // return ( diff --git a/src/chat/Chat.tsx b/src/chat/Chat.tsx new file mode 100644 index 0000000..68e5e5b --- /dev/null +++ b/src/chat/Chat.tsx @@ -0,0 +1,125 @@ +import { Box, CircularProgress } from "@mui/material"; +import { useGlobalState, useNoteAPI, useTeamAPI } from "providers"; +import React from "react"; +import { useCallback } from "react"; +import { useEffect } from "react"; +import { useState } from "react"; +import ChatInput from "./ChatInput"; +import ChatOutput from "./ChatOutput"; +import { Note } from "models"; +import { pond } from "protobuf-ts/pond"; +import moment from "moment-timezone"; + +interface Props { + objectKey: string; + type?: pond.NoteType; +} + +export default function Chat(props: Props) { + const [notes, setNotes] = useState([]); + const { objectKey, type } = props; + const noteAPI = useNoteAPI(); + const [notesLoaded, setNotesLoaded] = useState(false); + const [totalMessages, setTotalMessages] = useState(0); + const [scrollPos, setScrollPos] = useState(0); + const [{ team }] = useGlobalState(); + const [attachmentMap, setAttachmentMap] = useState>(new Map()); + + const teamAPI = useTeamAPI(); + + const loadNotes = useCallback(() => { + let currentNotes: Note[] = notes; + noteAPI + .listNotes(15, currentNotes.length, "desc", "timestamp", objectKey) + .then(resp => { + setNotesLoaded(true); + if (resp.data.notes.length < 1) { + return; + } + + setTotalMessages(resp.data.total); + let tempNotes: Note[] = []; + let tempAttachmentMap: Map = attachmentMap; + resp.data.notes.forEach(note => { + let n = Note.any(note); + tempNotes.push(n); + let refList = resp.data.attachments[n.key()]; + if (refList) { + tempAttachmentMap.set(n.key(), refList.refs); + } + }); + setScrollPos(tempNotes.length); + currentNotes = currentNotes.concat(tempNotes); + currentNotes.reverse(); + setNotes(currentNotes); + setAttachmentMap(tempAttachmentMap); + + if (type) { + // Setting the note type for future use + currentNotes.forEach(note => { + if (note.settings.objectType === pond.NoteType.NOTE_TYPE_UNKNOWN && type) { + note.settings.objectType = type; + noteAPI.updateNote(note.settings); + } + }); + + // Update new chat viewed timestamp + if (type === pond.NoteType.NOTE_TYPE_TEAM) { + team.preferences.chatViewedTimestamp = moment().toISOString(); + teamAPI.updatePreferences(team.key(), team.preferences); + } + } + }) + .catch(_err => {}); + }, [noteAPI, objectKey, notes, type, team, teamAPI, attachmentMap]); + + const removeNote = (index: number) => { + let n = notes; + setTotalMessages(totalMessages - 1); + n.splice(index, 1); + setNotes([...n]); + }; + + const showNewNotes = (newNote: Note) => { + let n = notes; + n.push(newNote); + setScrollPos(n.length); + setNotes([...n]); + }; + + useEffect(() => { + if (!notesLoaded) { + loadNotes(); + } + // setNotesLoaded(true); + }, [notesLoaded, loadNotes]); + + const loadMore = () => { + let n = notes; + n.reverse(); + setNotes(n); + loadNotes(); + }; + + return ( + + {!notesLoaded ? ( + + + + ) : ( +
+ + +
+ )} +
+ ); +} diff --git a/src/chat/ChatInput.tsx b/src/chat/ChatInput.tsx new file mode 100644 index 0000000..5fc1c30 --- /dev/null +++ b/src/chat/ChatInput.tsx @@ -0,0 +1,205 @@ +import { + Button, + //DialogActions, + //DialogContent, + //DialogTitle, + Grid2 as Grid, + //IconButton, + TextareaAutosize, + Theme, + Typography, + useTheme +} from "@mui/material"; +//import { pond } from "protobuf-ts/pond"; +import { useGlobalState, useNoteAPI, useSnackbar } from "providers"; +import React, { useState } from "react"; +// import { getThemeType } from "theme"; +import ChatIcon from "@mui/icons-material/Chat"; +import { Note } from "models"; +import { pond } from "protobuf-ts/pond"; +import { getThemeType } from "theme/themeType"; +import { makeStyles } from "@mui/styles"; +//import { AttachFile } from "@material-ui/icons"; +//import ResponsiveDialog from "common/ResponsiveDialog"; +//import FileUploader from "common/FileUploads/FileUploader"; +//import { cloneDeep } from "lodash"; + +const useStyles = makeStyles((theme: Theme) => ({ + iconBox: { + width: 36, + height: 36, + margin: 8, + marginRight: 12 + }, + textAreaClass: { + backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)", + borderColor: getThemeType() === "light" ? "rgb(255, 255, 255)" : "rgb(60, 60, 60)", + color: getThemeType() === "light" ? "rgb(10, 10, 10)" : "rgb(245, 245, 245)", + borderRadius: "5px", + width: "100%" + } +})) + +interface Props { + objectKey: string; + newNoteMethod: (note: Note) => void; + type?: pond.NoteType; +} + +/** + * renders the text area to enter you message and the button to submit it + * @returns text area to enter your message + */ +export default function ChatInput(props: Props) { + const [message, setMessage] = useState(""); + const classes = useStyles(); + const { objectKey, type } = props; + const [{ user }] = useGlobalState(); + const noteAPI = useNoteAPI(); + const { openSnack } = useSnackbar(); + const [shift, setShift] = useState(false); + const theme = useTheme(); + //const [attachmentDialogOpen, setAttachmentDialogOpen] = useState(false); + //const [uploadingFile, setUploadingFile] = useState(false); + const [uploadedFiles, setUploadedFiles] = useState>(new Map()); + + const clearEntry = () => { + setMessage(""); + }; + + const onKeyDown = (event: any) => { + if (event.key === "Shift") { + setShift(true); + } + if (event.key === "Enter" && !shift) { + event.preventDefault(); + } + }; + + const onKeyUp = (event: any) => { + if (event.key === "Shift") { + setShift(false); + } + if (event.key === "Enter" && !shift) { + validateMessage(); + } + }; + + const validateMessage = () => { + let valid = false; + let i = 0; + while (!valid && i < message.length) { + if (message[i] !== " " && message[i] !== "\n" && message[i] !== undefined) { + valid = true; + } + i++; + } + valid ? submit() : openSnack("Invalid Message"); + clearEntry(); + setUploadedFiles(new Map()); + }; + + const submit = () => { + if (message !== "" && message !== "\n") { + let newNote = Note.create(); + newNote.settings.objectKey = objectKey; + newNote.settings.userId = user.id(); + //newNote.settings.objectType = pond.NoteType.NOTE_TYPE_TEAM not using type at the moment + if (type) { + newNote.settings.objectType = type; + } + newNote.settings.timestamp = Date.now(); + newNote.settings.content = message; + + noteAPI + .addNote(newNote.settings, Array.from(uploadedFiles.keys())) + .then(resp => { + props.newNoteMethod(newNote); + openSnack("Message Sent"); + }) + .catch(err => { + openSnack("Message Failed to send"); + }); + } + clearEntry(); + }; + + /** + * Dialog to select attachments + * postponing note attachments until notes can be overhauled to be more in line with the rest of the objects + * @returns + */ + // const attachmentDialog = () => { + // return ( + // { + // setAttachmentDialogOpen(false); + // }}> + // Add Attachments + // + // { + // setUploadingFile(true); + // }} + // uploadEnd={(fileID, fileName) => { + // setUploadingFile(false); + // if (fileID && fileName) { + // let f = cloneDeep(uploadedFiles); + // f.set(fileID, fileName); + // setUploadedFiles(f); + // } + // }} + // /> + // + // + // + // + // + // ); + // }; + + return ( + + + + + + + setMessage(e.target.value)} + onKeyDown={onKeyDown} + onKeyUp={onKeyUp} + /> + + {/* + { + setAttachmentDialogOpen(true); + }} + color="primary"> + + + */} + + {Array.from(uploadedFiles.values()).toString()} + + ); +} diff --git a/src/chat/ChatMessage.tsx b/src/chat/ChatMessage.tsx new file mode 100644 index 0000000..f13ee31 --- /dev/null +++ b/src/chat/ChatMessage.tsx @@ -0,0 +1,288 @@ +import { + Avatar, + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid2 as Grid, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Menu, + Theme, + Typography +} from "@mui/material"; +import { MoreVert, Person, Delete as DeleteIcon, } from "@mui/icons-material"; +import moment from "moment-timezone"; +import { useGlobalState, useNoteAPI, useSnackbar, useUserAPI } from "providers"; +import React, { useState } from "react"; +// import { getThemeType } from "theme"; +// import { red } from "@material-ui/core/colors"; +import { useEffect } from "react"; +import { noteScope } from "models/Scope"; +import { pond } from "protobuf-ts/pond"; +import { useCallback } from "react"; +import { Note } from "models"; +import { red } from "@mui/material/colors"; +import { getThemeType } from "theme/themeType"; +import { makeStyles } from "@mui/styles"; +//import ImageViewer from "common/ImageViewer"; +//import ResponsiveDialog from "common/ResponsiveDialog"; + +interface Props { + index: number; + note: Note; + attachments?: pond.FileReference[]; + removeNoteMethod: (index: number) => void; +} +const useStyles = makeStyles((_theme: Theme) => ({ + red: { + color: red["500"] + }, + oddGrid: { + backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)", + padding: 4 + }, + evenGrid: { + backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)", + padding: 4 + }, + userAvatar: { + width: 36, + height: 36 + } +})) + +/** + * Takes in a note and renders it + * handles deleting of the note + * @param props Note object + */ +export default function ChatMessage(props: Props) { + const classes = useStyles(); + const [{ user }] = useGlobalState(); + const noteAPI = useNoteAPI(); + const { openSnack } = useSnackbar(); + const [permissions, setPermissions] = useState([]); + const userAPI = useUserAPI(); + const [dialog, setDialog] = useState(false); + const [anchorEl, setAnchorEl] = React.useState(null); + //const [fileListOpen, setFileListOpen] = useState(false); + + const [name, setName] = useState(""); + const [avatar, setAvatar] = useState(""); + + const loadPermissions = useCallback(() => { + let scope = noteScope(props.note.key()); + userAPI + .getUser(user.id(), scope) + .then(resp => { + setPermissions(resp.permissions); + }) + .catch(err => {}); + userAPI + .getProfile(props.note.settings.userId) + .then(resp => { + setName(resp.name); + setAvatar(resp.avatar); + }) + .catch(err => {}); + }, [props.note, userAPI, user]); + + useEffect(() => { + loadPermissions(); + }, [loadPermissions]); + + const deleteMessage = () => { + setAnchorEl(null); + noteAPI + .removeNote(props.note.key()) + .then(resp => { + openSnack("Message Deleted"); + props.removeNoteMethod(props.index); + }) + .catch(err => { + openSnack("Failed to delete Message"); + }); + closeDialog(); + }; + + const openDialog = () => { + setDialog(true); + }; + + const closeDialog = () => { + setDialog(false); + }; + + // const uploadedImages = () => { + // return ( + // + // {props.attachments?.map(attachment => { + // if (attachment.metadata && attachment.metadata.contentType.split("/")[0] === "image") { + // return ( + // + // + // + // ); + // } else { + // return undefined; + // } + // })} + // + // ); + // }; + + // const fileListDialog = () => { + // return ( + // { + // setFileListOpen(false); + // }}> + // Message Attachments + // + // {props.attachments && + // props.attachments.map(file => { + // if (file.metadata && file.metadata.contentType.includes("image/")) { + // //if the file is an image + // return ( + // + // + // + // ); + // } else { + // return undefined; + // } + // })} + // + // + // + // + // + // ); + // }; + + const messageMenu = () => { + return ( + setAnchorEl(null)} + keepMounted> + + + + + + Delete Message + + {/* {props.attachments && props.attachments.length > 0 && ( + { + setFileListOpen(true); + }}> + + + + View Attachments + + )} */} + + + ); + }; + + const deleteDialog = () => { + return ( + + Delete Message? + Are you sure you wish to delete this message + + + + + + + ); + }; + + return ( + + + + + {/* avatar */} + {avatar ? ( + + {!avatar && name} + + ) : ( + + + + )} + + + + + + + + + + {name} + + + + + + {moment(props.note.date()) + .tz(user.settings.timezone) + .format("MMMM Do YYYY, h:mm:ss a")} + + + + + + + + {props.note.content()} + + {/*load files and display attached images below the message */} + + + + + + {/* delete button */} + {permissions.includes(pond.Permission.PERMISSION_WRITE) && ( + + )} + + + {deleteDialog()} + {messageMenu()} + + ); +} diff --git a/src/chat/ChatOutput.tsx b/src/chat/ChatOutput.tsx new file mode 100644 index 0000000..73b7f55 --- /dev/null +++ b/src/chat/ChatOutput.tsx @@ -0,0 +1,86 @@ +import { useEffect, useRef } from "react"; +import ChatMessage from "./ChatMessage"; +import { Box, Button } from "@mui/material"; + +import { Note } from "models"; +import { pond } from "protobuf-ts/pond"; + +interface Props { + messages: Note[]; + attachmentMap?: Map; + totalMessages: number; + scrollPos: number; + removeNoteMethod: (index: number) => void; + loadMore: () => void; +} + +/** + * will load chat messages for the passed in object and display as a list of chat messages + * @returns List of chat messages + */ +export default function ChatOutput(props: Props) { + const ScrollTo = () => { + const elementRef = useRef(null); + useEffect(() => + elementRef?.current?.scrollIntoView({ + block: "end" + }) + ); + let elem =
; + return elem; + }; + + // const LoadMore = () => { + // const elementRef = useRef(null); + + // let elem = + //
+ // + //
; + + // if(CheckVisible(elementRef, "0px")) + // { + // props.loadMore() + // } + + // return elem; + // } + + const seeMore = () => { + props.loadMore(); + }; + + const displayMessages = () => { + let display = props.messages.map((curMessage, index) => ( +
+ + {props.scrollPos !== 0 && index === props.scrollPos && } +
+ )); + return display; + }; + + return ( + + {props.messages.length < props.totalMessages && ( +
+ +
+ )} + {displayMessages()} + {props.scrollPos === 0 && } +
+ ); +} diff --git a/src/models/Note.ts b/src/models/Note.ts new file mode 100644 index 0000000..0535566 --- /dev/null +++ b/src/models/Note.ts @@ -0,0 +1,45 @@ +import { cloneDeep } from "lodash"; +import { pond } from "protobuf-ts/pond"; +import { or } from "utils/types"; + +export class Note { + public settings: pond.NoteSettings = pond.NoteSettings.create(); + public status: pond.NoteStatus = pond.NoteStatus.create(); + + public static create(pb?: pond.Note): Note { + let my = new Note(); + if (pb) { + my.settings = pond.NoteSettings.fromObject(cloneDeep(or(pb.settings, {}))); + my.status = pond.NoteStatus.fromObject(cloneDeep(or(pb.status, {}))); + } + return my; + } + + public static clone(other?: Note): Note { + if (other) { + return Note.create( + pond.Note.fromObject({ + settings: cloneDeep(other.settings), + status: cloneDeep(other.status) + }) + ); + } + return Note.create(); + } + + public static any(data: any): Note { + return Note.create(pond.Note.fromObject(cloneDeep(data))); + } + + public key(): string { + return this.settings.key; + } + + public date(): number { + return this.settings.timestamp; + } + + public content(): string { + return this.settings.content; + } +} diff --git a/src/models/index.ts b/src/models/index.ts index ee21f45..ef5ba61 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -19,7 +19,7 @@ export * from "./user"; export * from "./team"; // export * from "./HarvestPlan"; // export * from "./HarvestYear"; -// export * from "./Note"; +export * from "./Note"; // export * from "./FieldMapping"; // export * from "./FieldMarker"; // export * from "./GrainBag"; diff --git a/src/pages/Team.tsx b/src/pages/Team.tsx index 1bc4c85..745d216 100644 --- a/src/pages/Team.tsx +++ b/src/pages/Team.tsx @@ -1,19 +1,33 @@ -import { Box, Grid2, Theme, Typography } from "@mui/material"; +import { Avatar, AvatarGroup, Box, Button, Grid2, Theme, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; import LoadingScreen from "app/LoadingScreen"; import { useMobile } from "hooks"; import { cloneDeep } from "lodash"; -import { Team } from "models"; +import { Team, teamScope, User } from "models"; // import { useSnackbar } from "notistack"; import PageContainer from "pages/PageContainer"; import { getContextKeys, getContextTypes } from "pbHelpers/Context"; -import { useSnackbar, useTeamAPI } from "providers"; +import { useSnackbar, useTeamAPI, useUserAPI } from "providers"; import { useEffect, useState } from "react"; import { useLocation, useParams } from 'react-router-dom'; +import { getSignatureAccentColour } from "services/whiteLabel"; import TeamActions from "teams/TeamActions"; +import ObjectUsers from "user/ObjectUsers"; import UserAvatar from "user/UserAvatar"; +import { or } from "utils/types"; -const useStyles = makeStyles((_theme: Theme) => ({ +const useStyles = makeStyles((theme: Theme) => ({ + avatar: { + width: theme.spacing(4), + height: theme.spacing(4) + }, + button: { + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + textTransform: "capitalize", + color: getSignatureAccentColour(), + }, container: { display: 'flex', justifyContent: "space-between", @@ -39,6 +53,7 @@ const useStyles = makeStyles((_theme: Theme) => ({ export default function TeamPage() { const teamAPI = useTeamAPI(); + const userAPI = useUserAPI(); const snackbar = useSnackbar() const classes = useStyles(); const { state } = useLocation(); @@ -46,7 +61,10 @@ export default function TeamPage() { const { teamID } = useParams<{ teamID: string }>(); // const [{ user }, dispatch] = useGlobalState(); const [team, setTeam] = useState(state?.team ? Team.create(state.team) : Team.create()) + const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false) + const [loadingUsers, setLoadingUsers] = useState(false) + const [userDialog, setUserDialog] = useState(false) const teamKey = teamID ? teamID : ""; const isMobile = useMobile(); @@ -56,11 +74,26 @@ export default function TeamPage() { setLoading(true) teamAPI.getTeam(teamKey).then(resp => { setTeam(resp) + }).catch(() => { + snackbar.error("Error loading team") }).finally(() => { setLoading(false) }) } + const listTeamUsers = () => { + setLoadingUsers(true) + userAPI.listObjectUsers(teamScope(teamKey)).then(resp => { + let rUsers: User[] = []; + or(resp.data, { users: [] }).users.forEach((user: any) => { + rUsers.push(User.any(user)); + }); + setUsers(rUsers); + }).finally(() => { + setLoadingUsers(false) + }) + } + const toggleNotificationPreference = () => { let updatedPreferences = cloneDeep(team.preferences); updatedPreferences.notify = !team.preferences.notify; @@ -86,6 +119,10 @@ export default function TeamPage() { getTeam(); }, []); + useEffect(() => { + listTeamUsers() + }, [teamKey]); + if (loading) return ( ) @@ -117,6 +154,33 @@ export default function TeamPage() { ) } + + const userAvatars = () => { + if (loadingUsers) return null + return ( + + + + + ) + } return ( @@ -134,6 +198,15 @@ export default function TeamPage() { /> + {userAvatars()} + setUserDialog(false)} + refreshCallback={() => {}} + /> ); } diff --git a/src/providers/index.ts b/src/providers/index.ts index 3296f88..2733726 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -6,7 +6,7 @@ export { // useBackpackAPI, useBinAPI, // useBinYardAPI, - // useNoteAPI, + useNoteAPI, // useComponentAPI, // useComponentsWebsocket, // useComponentWebsocket, diff --git a/src/providers/pond/noteAPI.tsx b/src/providers/pond/noteAPI.tsx new file mode 100644 index 0000000..e302158 --- /dev/null +++ b/src/providers/pond/noteAPI.tsx @@ -0,0 +1,89 @@ +import { AxiosResponse } from "axios"; +import { useHTTP } from "hooks"; +import { pond } from "protobuf-ts/pond"; +import { createContext, PropsWithChildren, useContext } from "react"; +import { pondURL } from "./pond"; + +export interface INoteAPIContext { + addNote: (note: pond.NoteSettings, attachments?: string[]) => Promise; + getNote: (noteID: string) => Promise; + updateNote: (note: pond.NoteSettings) => Promise; + listNotes: ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + asRoot?: boolean, + as?: string + ) => Promise>; + removeNote: (noteID: string) => Promise>; +} + +export const NoteAPIContext = createContext({} as INoteAPIContext); + +interface Props {} + +export default function NoteProvider(props: PropsWithChildren) { + const { children } = props; + const { get, del, post, put } = useHTTP(); + + const addNote = (note: pond.NoteSettings, attachments?: string[]) => { + return post( + pondURL("/notes" + (attachments ? "?attachments=" + attachments.toString() : "")), + note + ); + }; + + const getNote = (noteID: string) => { + return get(pondURL("/notes/" + noteID)); + }; + + const updateNote = (note: pond.NoteSettings) => { + return put(pondURL("/notes/"), note); + }; + + const removeNote = (key: string) => { + return del(pondURL("/notes/" + key)); + }; + + const listNotes = ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + asRoot?: boolean, + as?: string + ) => { + return get( + pondURL( + "/notes" + + "?limit=" + + limit + + "&offset=" + + offset + + ("&order=" + (order ? order : "asc")) + + ("&by=" + (orderBy ? orderBy : "key")) + + (search ? "&search=" + search : "") + + (asRoot ? "&asRoot=" + asRoot.toString() : "") + + (as ? "&as=" + as : "") + ) + ); + }; + + return ( + + {children} + + ); +} + +export const useNoteAPI = () => useContext(NoteAPIContext); diff --git a/src/providers/pond/pond.tsx b/src/providers/pond/pond.tsx index 37a8672..65eeca6 100644 --- a/src/providers/pond/pond.tsx +++ b/src/providers/pond/pond.tsx @@ -6,6 +6,8 @@ import PermissionProvider, { usePermissionAPI } from "./permissionAPI"; import { Scope } from "models"; import BinProvider, { useBinAPI } from "./binAPI"; import GateProvider, { useGateAPI } from "./gateAPI"; +import NoteProvider, { useNoteAPI } from "./noteAPI"; +// import NoteProvider from "providers/noteAPI"; export const pondURL = (partial: string, demo: boolean = false): string => { let url = import.meta.env.VITE_APP_API_URL + (demo ? "/demo" : "") + partial; @@ -27,7 +29,9 @@ export default function PondProvider(props: PropsWithChildren) { - {children} + + {children} + @@ -42,5 +46,6 @@ export { useImagekitAPI, usePermissionAPI, useBinAPI, - useGateAPI + useGateAPI, + useNoteAPI, }; diff --git a/src/providers/pond/userAPI.tsx b/src/providers/pond/userAPI.tsx index 614dbe6..2f6920d 100644 --- a/src/providers/pond/userAPI.tsx +++ b/src/providers/pond/userAPI.tsx @@ -26,7 +26,7 @@ export interface IUserAPIContext { by?: string, search?: string ) => Promise; -// listProfiles: () => Promise; + listProfiles: () => Promise; listObjectUsers: (scope: Scope) => Promise; updateObjectUsers: (scope: Scope, users: pond.IUser[]) => Promise; getUser: (id: string, scope?: Scope) => Promise; @@ -35,7 +35,7 @@ export interface IUserAPIContext { team?: string, scope?: any ) => Promise>; - // getProfile: (id: string) => Promise; + getProfile: (id: string) => Promise; // getUsers: (ids: string[]) => Promise; // getProfiles: (ids: string[]) => Promise; // getUserSettings: (userID: string) => Promise; @@ -91,19 +91,19 @@ export default function UserProvider(props: PropsWithChildren) { }); }; - // const listProfiles = (): Promise => { - // return new Promise((resolve, reject) => { - // get(pondURL("/profiles/")) - // .then((res: any) => { - // let profiles: pond.UserProfile[] = []; - // if (res && res.data && res.data.users) { - // res.data.users.forEach((raw: any) => profiles.push(pond.UserProfile.fromObject(raw))); - // } - // resolve(profiles); - // }) - // .catch((err: any) => reject(err)); - // }); - // }; + const listProfiles = (): Promise => { + return new Promise((resolve, reject) => { + get(pondURL("/profiles/")) + .then((res: any) => { + let profiles: pond.UserProfile[] = []; + if (res && res.data && res.data.users) { + res.data.users.forEach((raw: any) => profiles.push(pond.UserProfile.fromObject(raw))); + } + resolve(profiles); + }) + .catch((err: any) => reject(err)); + }); + }; const listObjectUsers = (scope: Scope) => { let sql = "/" + scope.kind + "s/" + scope.key + "/users"; @@ -161,13 +161,13 @@ export default function UserProvider(props: PropsWithChildren) { }); }; - // const getProfile = (id: string): Promise => { - // return new Promise((resolve, reject) => { - // get(pondURL("/profiles/" + id)) - // .then((res: any) => resolve(pond.UserProfile.fromObject(res.data))) - // .catch((err: any) => reject(err)); - // }); - // }; + const getProfile = (id: string): Promise => { + return new Promise((resolve, reject) => { + get(pondURL("/profiles/" + id)) + .then((res: any) => resolve(pond.UserProfile.fromObject(res.data))) + .catch((err: any) => reject(err)); + }); + }; // const getUsers = (ids: string[]): Promise => { // return new Promise((resolve, reject) => { @@ -232,12 +232,12 @@ export default function UserProvider(props: PropsWithChildren) { - - - + + + + + +