team chat is now functional
This commit is contained in:
parent
ba77dbc710
commit
7c3b0a3ab0
13 changed files with 1004 additions and 41 deletions
|
|
@ -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);
|
||||
}}>
|
||||
<Chat />
|
||||
<ChatIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -92,7 +116,27 @@ export default function HeaderButtons(props: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
chatButton()
|
||||
<>
|
||||
{chatButton()}
|
||||
{team && team.settings && team.settings.key && (
|
||||
<Drawer
|
||||
anchor="right"
|
||||
open={chatDrawerOpen}
|
||||
onClose={() => setChatDrawerOpen(false)}
|
||||
style={{ height: "100%", maxWidth: "100%" }}
|
||||
title={team.settings.name ? team.settings.name + "Chat" : "Chat"}>
|
||||
<div className={isMobile ? classes.chatDrawerMobile : classes.chatDrawer}>
|
||||
<div className={classes.drawerHeader}>
|
||||
<IconButton onClick={() => setChatDrawerOpen(false)}>
|
||||
<ChevronRight />
|
||||
</IconButton>
|
||||
<Typography>{team.name()} Chat</Typography>
|
||||
</div>
|
||||
<Chat objectKey={team.settings.key} type={pond.NoteType.NOTE_TYPE_TEAM} />
|
||||
</div>
|
||||
</Drawer>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
// return (
|
||||
|
|
|
|||
125
src/chat/Chat.tsx
Normal file
125
src/chat/Chat.tsx
Normal file
|
|
@ -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<Note[]>([]);
|
||||
const { objectKey, type } = props;
|
||||
const noteAPI = useNoteAPI();
|
||||
const [notesLoaded, setNotesLoaded] = useState<boolean>(false);
|
||||
const [totalMessages, setTotalMessages] = useState(0);
|
||||
const [scrollPos, setScrollPos] = useState(0);
|
||||
const [{ team }] = useGlobalState();
|
||||
const [attachmentMap, setAttachmentMap] = useState<Map<string, pond.FileReference[]>>(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<string, pond.FileReference[]> = 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 (
|
||||
<React.Fragment>
|
||||
{!notesLoaded ? (
|
||||
<Box marginTop={"51%"} marginLeft={"44%"}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<div style={{ maxHeight: "100%", display: "flex", flexDirection: "column-reverse" }}>
|
||||
<ChatInput newNoteMethod={showNewNotes} objectKey={objectKey} type={type} />
|
||||
<ChatOutput
|
||||
totalMessages={totalMessages}
|
||||
attachmentMap={attachmentMap}
|
||||
messages={notes}
|
||||
scrollPos={scrollPos}
|
||||
removeNoteMethod={removeNote}
|
||||
loadMore={loadMore}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
205
src/chat/ChatInput.tsx
Normal file
205
src/chat/ChatInput.tsx
Normal file
|
|
@ -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<Map<string, string>>(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 (
|
||||
// <ResponsiveDialog
|
||||
// open={attachmentDialogOpen}
|
||||
// onClose={() => {
|
||||
// setAttachmentDialogOpen(false);
|
||||
// }}>
|
||||
// <DialogTitle>Add Attachments</DialogTitle>
|
||||
// <DialogContent>
|
||||
// <FileUploader
|
||||
// uploadStart={() => {
|
||||
// setUploadingFile(true);
|
||||
// }}
|
||||
// uploadEnd={(fileID, fileName) => {
|
||||
// setUploadingFile(false);
|
||||
// if (fileID && fileName) {
|
||||
// let f = cloneDeep(uploadedFiles);
|
||||
// f.set(fileID, fileName);
|
||||
// setUploadedFiles(f);
|
||||
// }
|
||||
// }}
|
||||
// />
|
||||
// </DialogContent>
|
||||
// <DialogActions>
|
||||
// <Button
|
||||
// disabled={uploadingFile}
|
||||
// onClick={() => {
|
||||
// setAttachmentDialogOpen(false);
|
||||
// }}>
|
||||
// Close
|
||||
// </Button>
|
||||
// </DialogActions>
|
||||
// </ResponsiveDialog>
|
||||
// );
|
||||
// };
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid
|
||||
container
|
||||
direction="row-reverse"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
justifyContent="space-between"
|
||||
spacing={1}
|
||||
style={{ marginTop: theme.spacing(1) }}>
|
||||
<Grid>
|
||||
<Button onClick={validateMessage} variant="contained" color="primary">
|
||||
<ChatIcon />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 9 }} >
|
||||
<TextareaAutosize
|
||||
value={message}
|
||||
className={classes.textAreaClass}
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
</Grid>
|
||||
{/* <Grid item xs={2}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setAttachmentDialogOpen(true);
|
||||
}}
|
||||
color="primary">
|
||||
<AttachFile />
|
||||
</IconButton>
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
<Typography>{Array.from(uploadedFiles.values()).toString()}</Typography>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
288
src/chat/ChatMessage.tsx
Normal file
288
src/chat/ChatMessage.tsx
Normal file
|
|
@ -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<pond.Permission[]>([]);
|
||||
const userAPI = useUserAPI();
|
||||
const [dialog, setDialog] = useState(false);
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(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 (
|
||||
// <Grid container direction="row">
|
||||
// {props.attachments?.map(attachment => {
|
||||
// if (attachment.metadata && attachment.metadata.contentType.split("/")[0] === "image") {
|
||||
// return (
|
||||
// <Grid item xs={4} key={attachment.uuid}>
|
||||
// <ImageViewer image={attachment} width={"100%"} />
|
||||
// </Grid>
|
||||
// );
|
||||
// } else {
|
||||
// return undefined;
|
||||
// }
|
||||
// })}
|
||||
// </Grid>
|
||||
// );
|
||||
// };
|
||||
|
||||
// const fileListDialog = () => {
|
||||
// return (
|
||||
// <ResponsiveDialog
|
||||
// open={fileListOpen}
|
||||
// onClose={() => {
|
||||
// setFileListOpen(false);
|
||||
// }}>
|
||||
// <DialogTitle>Message Attachments</DialogTitle>
|
||||
// <DialogContent>
|
||||
// {props.attachments &&
|
||||
// props.attachments.map(file => {
|
||||
// if (file.metadata && file.metadata.contentType.includes("image/")) {
|
||||
// //if the file is an image
|
||||
// return (
|
||||
// <Box margin={2} key={file.uuid}>
|
||||
// <ImageViewer image={file} width={"100%"} />
|
||||
// </Box>
|
||||
// );
|
||||
// } else {
|
||||
// return undefined;
|
||||
// }
|
||||
// })}
|
||||
// </DialogContent>
|
||||
// <DialogActions>
|
||||
// <Button onClick={() => setFileListOpen(false)}>Close</Button>
|
||||
// </DialogActions>
|
||||
// </ResponsiveDialog>
|
||||
// );
|
||||
// };
|
||||
|
||||
const messageMenu = () => {
|
||||
return (
|
||||
<Menu
|
||||
id="groupMenu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
keepMounted>
|
||||
<List>
|
||||
<ListItemButton onClick={openDialog}>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon className={classes.red} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Delete Message</ListItemText>
|
||||
</ListItemButton>
|
||||
{/* {props.attachments && props.attachments.length > 0 && (
|
||||
<ListItem
|
||||
button
|
||||
onClick={() => {
|
||||
setFileListOpen(true);
|
||||
}}>
|
||||
<ListItemIcon>
|
||||
<Visibility />
|
||||
</ListItemIcon>
|
||||
<ListItemText>View Attachments</ListItemText>
|
||||
</ListItem>
|
||||
)} */}
|
||||
</List>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const deleteDialog = () => {
|
||||
return (
|
||||
<Dialog open={dialog} onClose={closeDialog}>
|
||||
<DialogTitle>Delete Message?</DialogTitle>
|
||||
<DialogContent>Are you sure you wish to delete this message</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={closeDialog} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={deleteMessage} color="primary">
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box style={{ width: "100%" }}>
|
||||
<Grid
|
||||
wrap="nowrap"
|
||||
container
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
className={props.index % 2 === 0 ? classes.oddGrid : classes.evenGrid}>
|
||||
<Grid>
|
||||
<Box marginLeft="5px">
|
||||
{/* avatar */}
|
||||
{avatar ? (
|
||||
<Avatar alt={name} src={avatar} className={classes.userAvatar}>
|
||||
{!avatar && name}
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar alt={name} className={classes.userAvatar}>
|
||||
<Person />
|
||||
</Avatar>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Box marginLeft="5px">
|
||||
<Grid container direction="column" style={{ margin: "5px" }}>
|
||||
<Grid>
|
||||
<Grid container direction="row">
|
||||
<Grid>
|
||||
<Typography variant="subtitle2" color="textPrimary">
|
||||
{name}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Box marginRight={1} marginLeft={1}>
|
||||
<Grid>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{moment(props.note.date())
|
||||
.tz(user.settings.timezone)
|
||||
.format("MMMM Do YYYY, h:mm:ss a")}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Typography style={{ whiteSpace: "pre-wrap" }} variant="caption">
|
||||
{props.note.content()}
|
||||
</Typography>
|
||||
{/*load files and display attached images below the message */}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid>
|
||||
{/* delete button */}
|
||||
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
|
||||
<Button
|
||||
onClick={(event: React.MouseEvent<HTMLButtonElement>) =>
|
||||
setAnchorEl(event.currentTarget)
|
||||
}>
|
||||
<MoreVert />
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
{deleteDialog()}
|
||||
{messageMenu()}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
86
src/chat/ChatOutput.tsx
Normal file
86
src/chat/ChatOutput.tsx
Normal file
|
|
@ -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<string, pond.FileReference[]>;
|
||||
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 | HTMLDivElement>(null);
|
||||
useEffect(() =>
|
||||
elementRef?.current?.scrollIntoView({
|
||||
block: "end"
|
||||
})
|
||||
);
|
||||
let elem = <div ref={elementRef} />;
|
||||
return elem;
|
||||
};
|
||||
|
||||
// const LoadMore = () => {
|
||||
// const elementRef = useRef<null | HTMLDivElement>(null);
|
||||
|
||||
// let elem =
|
||||
// <div style={{textAlign: "center"}} ref={elementRef}>
|
||||
// <CircularProgress />
|
||||
// </div>;
|
||||
|
||||
// if(CheckVisible(elementRef, "0px"))
|
||||
// {
|
||||
// props.loadMore()
|
||||
// }
|
||||
|
||||
// return elem;
|
||||
// }
|
||||
|
||||
const seeMore = () => {
|
||||
props.loadMore();
|
||||
};
|
||||
|
||||
const displayMessages = () => {
|
||||
let display = props.messages.map((curMessage, index) => (
|
||||
<div key={index}>
|
||||
<ChatMessage
|
||||
note={curMessage}
|
||||
index={index}
|
||||
attachments={props.attachmentMap?.get(curMessage.key())}
|
||||
removeNoteMethod={props.removeNoteMethod}
|
||||
/>
|
||||
{props.scrollPos !== 0 && index === props.scrollPos && <ScrollTo />}
|
||||
</div>
|
||||
));
|
||||
return display;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
//height="85%"
|
||||
height="auto"
|
||||
style={{
|
||||
overflowY: "scroll",
|
||||
overflowX: "hidden",
|
||||
borderRadius: "4px"
|
||||
}}>
|
||||
{props.messages.length < props.totalMessages && (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Button onClick={seeMore}>See More</Button>
|
||||
</div>
|
||||
)}
|
||||
{displayMessages()}
|
||||
{props.scrollPos === 0 && <ScrollTo />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
45
src/models/Note.ts
Normal file
45
src/models/Note.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<Team>(state?.team ? Team.create(state.team) : Team.create())
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
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 (
|
||||
<LoadingScreen message="Loading team"/>
|
||||
)
|
||||
|
|
@ -118,6 +155,33 @@ export default function TeamPage() {
|
|||
)
|
||||
}
|
||||
|
||||
const userAvatars = () => {
|
||||
if (loadingUsers) return null
|
||||
return (
|
||||
<Grid2 container direction={"row"} justifyContent={"space-between"} paddingTop={1}>
|
||||
<Button className={classes.button} onClick={() => setUserDialog(true)}>
|
||||
<Grid2 container spacing={1}>
|
||||
<Grid2>
|
||||
<AvatarGroup max={3}>
|
||||
{users.map((user, index) => {
|
||||
return(
|
||||
<Avatar key={index} className={classes.avatar} alt={user.name()} src={user.settings.avatar}/>
|
||||
)
|
||||
})}
|
||||
</AvatarGroup>
|
||||
</Grid2>
|
||||
<Grid2 alignContent={"center"}>
|
||||
<Typography color="textPrimary" variant="body2">
|
||||
{users.length + " team members"}
|
||||
</Typography>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Button>
|
||||
<Grid2></Grid2>
|
||||
</Grid2>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer sx={{ padding: isMobile ? 1 : 2 }}>
|
||||
<Box className={classes.container}>
|
||||
|
|
@ -134,6 +198,15 @@ export default function TeamPage() {
|
|||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
{userAvatars()}
|
||||
<ObjectUsers
|
||||
scope={teamScope(teamKey)}
|
||||
label={team.name()}
|
||||
permissions={team.permissions}
|
||||
isDialogOpen={userDialog}
|
||||
closeDialogCallback={() => setUserDialog(false)}
|
||||
refreshCallback={() => {}}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export {
|
|||
// useBackpackAPI,
|
||||
useBinAPI,
|
||||
// useBinYardAPI,
|
||||
// useNoteAPI,
|
||||
useNoteAPI,
|
||||
// useComponentAPI,
|
||||
// useComponentsWebsocket,
|
||||
// useComponentWebsocket,
|
||||
|
|
|
|||
89
src/providers/pond/noteAPI.tsx
Normal file
89
src/providers/pond/noteAPI.tsx
Normal file
|
|
@ -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<any>;
|
||||
getNote: (noteID: string) => Promise<any>;
|
||||
updateNote: (note: pond.NoteSettings) => Promise<any>;
|
||||
listNotes: (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
asRoot?: boolean,
|
||||
as?: string
|
||||
) => Promise<AxiosResponse<pond.ListNotesResponse>>;
|
||||
removeNote: (noteID: string) => Promise<AxiosResponse<pond.RemoveBinResponse>>;
|
||||
}
|
||||
|
||||
export const NoteAPIContext = createContext<INoteAPIContext>({} as INoteAPIContext);
|
||||
|
||||
interface Props {}
|
||||
|
||||
export default function NoteProvider(props: PropsWithChildren<Props>) {
|
||||
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<pond.RemoveNoteResponse>(pondURL("/notes/" + key));
|
||||
};
|
||||
|
||||
const listNotes = (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
asRoot?: boolean,
|
||||
as?: string
|
||||
) => {
|
||||
return get<pond.ListNotesResponse>(
|
||||
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 (
|
||||
<NoteAPIContext.Provider
|
||||
value={{
|
||||
addNote,
|
||||
getNote,
|
||||
updateNote,
|
||||
listNotes,
|
||||
removeNote
|
||||
}}>
|
||||
{children}
|
||||
</NoteAPIContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useNoteAPI = () => useContext(NoteAPIContext);
|
||||
|
|
@ -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<any>) {
|
|||
<PermissionProvider>
|
||||
<BinProvider>
|
||||
<GateProvider>
|
||||
{children}
|
||||
<NoteProvider>
|
||||
{children}
|
||||
</NoteProvider>
|
||||
</GateProvider>
|
||||
</BinProvider>
|
||||
</PermissionProvider>
|
||||
|
|
@ -42,5 +46,6 @@ export {
|
|||
useImagekitAPI,
|
||||
usePermissionAPI,
|
||||
useBinAPI,
|
||||
useGateAPI
|
||||
useGateAPI,
|
||||
useNoteAPI,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export interface IUserAPIContext {
|
|||
by?: string,
|
||||
search?: string
|
||||
) => Promise<ListUsersResponse>;
|
||||
// listProfiles: () => Promise<pond.UserProfile[]>;
|
||||
listProfiles: () => Promise<pond.UserProfile[]>;
|
||||
listObjectUsers: (scope: Scope) => Promise<any>;
|
||||
updateObjectUsers: (scope: Scope, users: pond.IUser[]) => Promise<any>;
|
||||
getUser: (id: string, scope?: Scope) => Promise<User>;
|
||||
|
|
@ -35,7 +35,7 @@ export interface IUserAPIContext {
|
|||
team?: string,
|
||||
scope?: any
|
||||
) => Promise<AxiosResponse<pond.GetUserWithTeamResponse>>;
|
||||
// getProfile: (id: string) => Promise<pond.UserProfile>;
|
||||
getProfile: (id: string) => Promise<pond.UserProfile>;
|
||||
// getUsers: (ids: string[]) => Promise<User[]>;
|
||||
// getProfiles: (ids: string[]) => Promise<pond.UserProfile[]>;
|
||||
// getUserSettings: (userID: string) => Promise<any>;
|
||||
|
|
@ -91,19 +91,19 @@ export default function UserProvider(props: PropsWithChildren<any>) {
|
|||
});
|
||||
};
|
||||
|
||||
// const listProfiles = (): Promise<pond.UserProfile[]> => {
|
||||
// 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<pond.UserProfile[]> => {
|
||||
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<any>) {
|
|||
});
|
||||
};
|
||||
|
||||
// const getProfile = (id: string): Promise<pond.UserProfile> => {
|
||||
// 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<pond.UserProfile> => {
|
||||
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<User[]> => {
|
||||
// return new Promise((resolve, reject) => {
|
||||
|
|
@ -232,12 +232,12 @@ export default function UserProvider(props: PropsWithChildren<any>) {
|
|||
<UserAPIContext.Provider
|
||||
value={{
|
||||
listUsers,
|
||||
// listProfiles,
|
||||
listProfiles,
|
||||
listObjectUsers,
|
||||
updateObjectUsers,
|
||||
getUser,
|
||||
getUserWithTeam,
|
||||
// getProfile,
|
||||
getProfile,
|
||||
// getUsers,
|
||||
// getProfiles,
|
||||
// getUserSettings,
|
||||
|
|
|
|||
|
|
@ -196,9 +196,6 @@ export default function UserMenu(props: Props) {
|
|||
)}
|
||||
</Box>
|
||||
</Button>
|
||||
<IconButton onClick={toggleTheme} className={classes.rightIcon} aria-label="Toggle Theme">
|
||||
<ThemeIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="userMenu"
|
||||
anchorEl={anchorEl}
|
||||
|
|
@ -211,6 +208,12 @@ export default function UserMenu(props: Props) {
|
|||
</ListItemIcon>
|
||||
<ListItemText primary="User Settings" />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={toggleTheme} aria-label="Toggle Theme" dense>
|
||||
<ListItemIcon>
|
||||
<ThemeIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Toggle Theme" />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout} aria-label="Sign Out" dense>
|
||||
<ListItemIcon>
|
||||
<ExitToApp className={classes.red} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue