diff --git a/functions/handlers/users.js b/functions/handlers/users.js index 7d2b5c0..f6dc0fc 100644 --- a/functions/handlers/users.js +++ b/functions/handlers/users.js @@ -163,11 +163,13 @@ exports.login = (req, res) => { return; }) .catch(function(err) { - if (!doc.exists) { - return res - .status(403) - .json({ general: "Invalid credentials. Please try again." }); - } + // FIX: doc variable is out of scope + // if (!doc.exists) { + // return res + // .status(403) + // .json({ general: "Invalid credentials. Please try again." }); + // } + console.log(err); return res.status(500).send(err); }); } @@ -552,6 +554,535 @@ exports.unverifyUser = (req, res) => { return res.status(500).json({ error: err.code }); }); }; + +// Returns all the DMs that the user is currently participating in +exports.getDirectMessages = (req, res) => { +/* Return value + * data: [DMs] + * dm : { + * dmId: str + * messages: [msgs] + * msg: { + * author: str + * createdAt: ISOString + * message: str + * messageId: str + * } + * recipient: str + * recentMessage: str + * recentMessageTimestamp: ISOString + * } + */ + + // Returns all the messages in a dm documentSnapshot + function getMessages(dm) { + let promise = new Promise((resolve, reject) => { + let messagesCollection = dm.collection('messages'); + + // If the messagesCollection is missing, that mean that there aren't any messages + if (messagesCollection === null || messagesCollection === undefined) { + return; + } + + let msgs = []; + let promises = []; + + // Get all of the messages in the DM + messagesCollection.get() + .then((dmQuerySnap) => { + dmQuerySnap.forEach((dmQueryDocSnap) => { + promises.push( + dmQueryDocSnap.ref.get() + .then((messageData) => { + msgs.push(messageData.data()); + return; + }) + ) + }) + + let waitPromise = Promise.all(promises); + waitPromise.then(() => { + // Sort the messages in reverse order by date + // Newest should be at the bottom, because that's how they will be displayed on the front-end + msgs.sort((a, b) => { + return (b.createdAt > a.createdAt) ? -1 : ((b.createdAt < a.createdAt) ? 1 : 0); + }) + resolve(msgs); + }); + }) + }); + return promise; + } + + + const dms = req.userData.dms; + + // Return null if this user has no DMs + if (dms === undefined || dms === null || dms.length === 0) return res.status(200).json({data: null}); + + let dmsData = []; + let dmPromises = []; + + dms.forEach((dm) => { + let dmData = {}; + // Make a new promise for each DM document + dmPromises.push(new Promise((resolve, reject) => { + dm // DM document reference + .get() + .then((doc) => { + let docData = doc.data(); + + // Recipient is the person you are messaging + docData.authors[0] === req.userData.handle ? + dmData.recipient = docData.authors[1] : + dmData.recipient = docData.authors[0] + + // Save the createdAt time + dmData.createdAt = docData.createdAt; + + // Get all the messages from this dm document + getMessages(dm) + .then((msgs) => { + dmData.messages = msgs; + dmData.recentMessage = msgs.length !== 0 ? msgs[msgs.length - 1].message : null; + dmData.recentMessageTimestamp = msgs.length !== 0 ? msgs[msgs.length - 1].createdAt : null; + dmData.dmId = doc.id; + resolve(dmData); + }) + + + + }).catch((err) => { + console.err(err); + return res.status(400).json({error: { + message: "An error occurred when reading the DM document reference", + error: err + }}); + }) + }).then((dmData) => { + dmsData.push(dmData); + }) + ) + + }) + + // Wait for all DM document promises to resolve before returning data + dmWaitPromise = Promise.all(dmPromises) + .then(() => { + // Sort the DMs so that the ones with the newest messages are at the top + dmsData.sort((a, b) => { + if (a.recentMessageTimestamp === null && b.recentMessageTimestamp === null) { + if (b.createdAt < a.createdAt) { + return -1; + } else if (b.createdAt > a.createdAt) { + return 1; + } else { + return 0; + } + } else if (a.recentMessageTimestamp === null) { + return 1; + } else if (b.recentMessageTimestamp === null) { + return -1; + } else if (b.recentMessageTimestamp < a.recentMessageTimestamp) { + return -1; + } else if (b.recentMessageTimestamp > a.recentMessageTimestamp) { + return 1; + } else { + return 0; + } + }); + return res.status(200).json({data: dmsData}) + }) + .catch((err) => { + return res.status(500).json({error:{ + message: "An error occurred while sorting", + error: err + }}); + }); +} + +// Toggles direct messages on or off depending on the requese +/* Request Parameters + * enable: bool + */ +exports.toggleDirectMessages = (req, res) => { + const enable = req.body.enable; + const user = req.userData.handle; + db.doc(`/users/${user}`).update({dmEnabled: enable}) + .then(() => { + return res.status(201).json({message: "Success"}); + }) + .catch((err) => { + return res.status(500).json({error: err}); + }) +} + +// Returns a promise that resolves if user has DMs enabled +// and rejects if there is an error or DMs are disabled +isDirectMessageEnabled = (username) => { + return new Promise((resolve, reject) => { + let result = {}; + result.code = null; + result.message = null; + if (username === null || username === undefined || username === "") { + result.code = 400; + result.message = "No user was sent in the request. The request should have a non-empty 'user' key."; + reject(result); + } + + db.doc(`/users/${username}`) + .get() + .then((doc) => { + if (doc.exists) { + // console.log(doc.data()) + if (doc.data().dmEnabled === true || doc.data().dmEnabled === null || doc.data().dmEnabled === undefined) { + // Assume DMs are enabled if they don't have a dmEnabled key + resolve(result); + } else { + result.code = 200; + result.message = `${username} has DMs disabled`; + reject(result); + } + } else { + console.log(`${username} is not in the database`); + result.code = 400; + result.message = `${username} is not in the database`; + reject(result); + } + }) + .catch((err) => { + console.log("HI") + console.error(err); + result.code = 500; + result.message = err; + reject(result); + }) + }); +} + +// Returns a promise that resolves if the data in the DM is valid and +// rejects if there are any error. Errors are returned in the promise +verifyDirectMessageIntegrity = (dmRef) => { + return new Promise((resolve, reject) => { + resolve("Not implemented yet"); + }) +} + + +// Checks if there are any DM channels open with userB on userA's side +oneWayCheck = (userA, userB) => { + return new Promise((resolve, reject) => { + + db.doc(`/users/${userA}`) + .get() + .then((userASnapshot) => { + const dmList = userASnapshot.data().dms; + const dmRecipients = userASnapshot.data().dmRecipients; + + if (dmList === null || dmList === undefined || dmRecipients === null || dmRecipients === undefined) { + // They don't have any DMs yet + console.log("No DMs array"); + userASnapshot.ref.set({dms:[], dmRecipients:[]}, {merge: true}) + .then(() => { + resolve(); + }) + } else if (dmList.length === 0) { + // Their DMs are empty + console.log("DMs array is empty"); + resolve(); + } else { + // let dmDocs = []; + // let forEachPromises = []; + // dmList.forEach((dmRef) => { + // forEachPromises.push( + // dmRef.get() + // // .then((dmDoc) => { + // // TODO: Figure out why dmDoc.exists() isn't working + // // Make sure all of the docs exist and none of the references + // // are broken + // // if (dmDoc.exists()) { + // // dmDocs.push(dmDoc); + // // } else { + // // console.log(`DM reference /dm/${dmDoc.id} is invalid`); + // // reject(`DM reference /dm/${dmDoc.id} is invalid`); + // // } + // // }) + // ) + // }) + + dmRecipients.forEach((dmRecipient) => { + if (dmRecipient === userB) { + console.log(`You already have a DM with ${userB}`); + reject(new Error(`You already have a DM with ${userB}`)); + return; + } + }) + + resolve(); + + + // Promise.all(forEachPromises) + // .then((dmDocs) => { + // // Check if any of the DMs have for userA have userA and userB as the authors. + // // This would mean that they already have a DM channel + // dmDocs.forEach((dmDoc) => { + // // Checking if any of the authors key in any of their DMs are missing + // let authors = dmDoc.data().authors; + + // // if (authors[0] === "keanureeves") { + // // console.log("it is") + // // resolve(); + // // } else { + // // console.log("it is not") + // // reject("not my keanu"); + // // } + // // if (authors === null || authors === undefined || authors.length !== 2) { + // // // console.log(`The authors key in /dm/${dmDoc.id} is undefined or missing values`); + // // // reject(`The authors key in /dm/${dmDoc.id} is undefined or missing values`); + // // console.log('a') + // // reject("a") + // // } else if ((authors[0] === userA && authors[1] === userB) || (authors[1] === userA && authors[0] === userB)) { + // // // console.log(`${userA} already has a DM channel between ${userA} and ${userB}`); + // // // reject(`${userA} already has a DM channel between ${userA} and ${userB}`); + // // console.log('b') + // // reject('b') + // // } else { + // // // BUG: For some reason the promise.all is resolving even though there are multiple rejects + // // // and only one resolve + // // console.log("c"); + // // resolve(); + // // } + // // console.log(authors) + // // console.log([userA, userB]) + // if (authors[0] === null || authors === undefined || authors.length !== 2) { + // console.log('a'); + // reject('a'); + // } else if (authors[0] === userA && authors[1] === userB) { + // console.log("b"); + // reject('b'); + // } else { + // console.log('c'); + // resolve(); + // } + // }) + // }) + + + + } + }) + }) + +} + + +// Returns a promise that resolves if there is not already a DM channel +// between the creator and recipient usernames. It rejects if one already +// exists or there is an error. +checkNoDirectMessageExists = (creator, recipient) => { + return new Promise((resolve, reject) => { + let creatorPromise = oneWayCheck(creator, recipient); + let recipientPromise = oneWayCheck(recipient, creator); + let temp_array = []; + temp_array.push(creatorPromise); + temp_array.push(recipientPromise) + + Promise.all(temp_array) + .then(() => { + resolve(); + }) + .catch((err) => { + reject(err); + }) + }) +} + +addDirectMessageToUser = (username, recipient, dmRef) => { + return new Promise((resolve, reject) => { + db.doc(`/users/${username}`).get() + .then((docSnap) => { + let dmList = docSnap.data().dms; + let dmRecipients = docSnap.data().dmRecipients; + dmList.push(dmRef); + dmRecipients.push(recipient); + return db.doc(`/users/${username}`).update({dms: dmList, dmRecipients}); + }) + .then(() => { + resolve(); + }) + .catch((err) => { + reject(err); + }) + }) +} + +// Sends a DM from the caller to the requested DM document +/* Request Parameters + * message: str + * user: str + */ +exports.sendDirectMessage = (req, res) => { + // TODO: add error checking for if message or user is null + const creator = req.userData.handle; + const recipient = req.body.user; + const message = req.body.message; + + const newMessage = { + author: creator, + createdAt: new Date().toISOString(), + message, + messageId: null + } + + db.doc(`/users/${creator}`).get() + .then((userDoc) => { + let dmList = userDoc.data().dms; + + // Return if the creator doesn't have any DMs. + // This means they have not created a DM's channel yet + if (dmList === null || dmList === undefined) return res.status(400).json({error: `There is no DM channel between ${creator} and ${recipient}. Use /api/dms/new.`}) + let dmRefPromises = []; + dmList.forEach((dmRef) => { + dmRefPromises.push( + new Promise((resolve, reject) => { + dmRef.get() + .then((dmDoc) => { + let authors = dmDoc.data().authors; + if ( + (authors[0] === creator && authors[1] === recipient) || + (authors[1] === creator && authors[0] === recipient) + ) { + resolve({correct: true, dmRef}); + } else { + resolve({correct: false, dmRef}); + } + }) + .catch((err) => { + reject(err); + }) + }) + ) + }) + + return Promise.all(dmRefPromises); + }) + .then((results) => { + let correctDMRef = null; + results.forEach((result) => { + if (result.correct) { + correctDMRef = result.dmRef; + } + }) + + if (correctDMRef === null) { + console.log(`There is no DM channel between ${creator} and ${recipient}. Use /api/dms/new.`); + return res.status(400).json({error: `There is no DM channel between ${creator} and ${recipient}. Use /api/dms/new.`}); + } + + return db.collection(`/dm/${correctDMRef.id}/messages`).add(newMessage); + }) + .then((newMsgRef) => { + return newMsgRef.update({messageId: newMsgRef.id}, {merge: true}); + }) + .then(() => { + return res.status(200).json({message: "OK"}); + }) + .catch((err) => { + console.log(err); + return res.status(500).json({error: err}); + }) +} + +// Creates a DM between the caller and the user in the request +/* Request Parameters + * user: str + */ +exports.createDirectMessage = (req, res) => { + const creator = req.userData.handle; + const recipient = req.body.user; + + // Check if they are DMing themselves + if (creator === recipient) return res.status(400).json({error: "You can't DM yourself"}); + + // Check if this user has DMs enabled + let creatorEnabled = isDirectMessageEnabled(creator); + + // Check if the requested user has DMs enabled + let recipientEnabled = isDirectMessageEnabled(recipient); + + // Make sure that they don't already have a DM channel + let noDMExists = checkNoDirectMessageExists(creator, recipient) + + + let dataValidations = [ + creatorEnabled, + recipientEnabled, + noDMExists + ] + + Promise.all(dataValidations) + .then(() => { + // Create a new DM document + return db.collection("dm").add({}) + }) + .then((dmDocRef) => { + // Fill it with some data. + // Note that there isn't a messages collection by default. + let dmData = { + dmId: dmDocRef.id, + authors: [creator, recipient], + createdAt: new Date().toISOString() + } + + // Update DM document + let dmDocPromise = dmDocRef.set(dmData); + + // Add the DM reference to the creator + let updateCreatorPromise = addDirectMessageToUser(creator, recipient, dmDocRef); + + // Add the DM reference to the recipient + let updateRecipientPromise = addDirectMessageToUser(recipient, creator, dmDocRef); + + // Wait for all promises + return Promise.all([dmDocPromise, updateCreatorPromise, updateRecipientPromise]); + }) + .then (() => { + return res.status(201).json({message: "Success!"}); + }) + .catch((err) => { + console.log(err); + + if (err.code && err.message && err.code > 0) { + // Specific error that I've created + return res.status(err.code).json({error: err.message}); + } else { + // Generic or firebase error + return res.status(500).json({error: err}); + } + }) +} + +// Checks if the requested user has DMs enable or not +/* Request Parameters + * user: str + */ +exports.checkDirectMessagesEnabled = (req, res) => { + isDirectMessageEnabled(req.body.user) + .then(() => { + return res.status(200).json({enabled: true}); + }) + .catch((result) => { + console.log(result); + if (result.code === 200) { + // DMs are disabled + return res.status(200).json({enabled: false}); + } else { + // Some other error occured + return res.status(result.code).json({err: result.message}); + } + }) +} + exports.getUserHandles = (req, res) => { db.doc(`/users/${req.body.userHandle}`) .get() diff --git a/functions/index.js b/functions/index.js index a224bf7..4392439 100644 --- a/functions/index.js +++ b/functions/index.js @@ -11,6 +11,11 @@ app.use(cors()); *------------------------------------------------------------------*/ const { getAuthenticatedUser, + getDirectMessages, + sendDirectMessage, + createDirectMessage, + checkDirectMessagesEnabled, + toggleDirectMessages, getAllHandles, getUserDetails, getProfileInfo, @@ -39,6 +44,23 @@ app.post("/login", login); //Deletes user account app.delete("/delete", fbAuth, deleteUser); +// Returns all direct messages that the user is participating in +app.get("/dms", fbAuth, getDirectMessages); + +// Send a message in a DM from one user to another +app.post("/dms/send", fbAuth, sendDirectMessage); + +// Create a new DM between two users +app.post("/dms/new", fbAuth, createDirectMessage); + +// Checks if the user provided has DMs enabled or not +app.post("/dms/enabled", checkDirectMessagesEnabled); + +// Used to toggle DMs on or off for the current user +app.post("/dms/toggle", fbAuth, toggleDirectMessages); + +app.get("/getUser", fbAuth, getUserDetails); + app.post("/getUserDetails", fbAuth, getUserDetails); // Returns a list of all usernames @@ -78,6 +100,7 @@ app.post("/addSubscription", fbAuth, addSubscription); // remove one subscription app.post("/removeSub", fbAuth, removeSub); + /*------------------------------------------------------------------* * handlers/post.js * *------------------------------------------------------------------*/ diff --git a/twistter-frontend/package.json b/twistter-frontend/package.json index 52e50f3..daf9b48 100644 --- a/twistter-frontend/package.json +++ b/twistter-frontend/package.json @@ -10,6 +10,7 @@ "axios": "^0.19.0", "clsx": "^1.0.4", "create-react-app": "^3.1.2", + "dayjs": "^1.8.17", "fuse.js": "^3.4.6", "install": "^0.13.0", "jwt-decode": "^2.2.0", @@ -22,7 +23,8 @@ "react-scripts": "0.9.5", "redux": "^4.0.4", "redux-thunk": "^2.3.0", - "typeface-roboto": "0.0.75" + "typeface-roboto": "0.0.75", + "underscore": "^1.9.1" }, "devDependencies": {}, "scripts": { diff --git a/twistter-frontend/src/App.js b/twistter-frontend/src/App.js index 1ed56dc..6dd4550 100644 --- a/twistter-frontend/src/App.js +++ b/twistter-frontend/src/App.js @@ -31,6 +31,7 @@ import editProfile from "./pages/editProfile"; import userLine from "./Userline.js"; import verify from "./pages/verify"; import Search from "./pages/Search.js"; +import directMessages from "./pages/directMessages"; import otherUser from "./pages/otherUser"; const theme = createMuiTheme(themeObject); @@ -62,7 +63,7 @@ class App extends Component {
-
+
{/* AuthRoute checks if the user is logged in and if they are it redirects them to /home */} @@ -77,6 +78,7 @@ class App extends Component { + diff --git a/twistter-frontend/src/components/layout/NavBar.js b/twistter-frontend/src/components/layout/NavBar.js index f1e1b4a..761db65 100644 --- a/twistter-frontend/src/components/layout/NavBar.js +++ b/twistter-frontend/src/components/layout/NavBar.js @@ -46,6 +46,11 @@ export class Navbar extends Component { Profile )} + {authenticated && ( + + )} {!authenticated && ( )} {authenticated && ( - )} diff --git a/twistter-frontend/src/pages/directMessages.js b/twistter-frontend/src/pages/directMessages.js new file mode 100644 index 0000000..eadadf9 --- /dev/null +++ b/twistter-frontend/src/pages/directMessages.js @@ -0,0 +1,658 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import _ from "underscore"; + +// Material UI +import Box from '@material-ui/core/Box'; +import Button from '@material-ui/core/Button'; +import Card from '@material-ui/core/Card'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Fab from '@material-ui/core/Fab'; +import Grid from '@material-ui/core/Grid'; +import Popover from '@material-ui/core/Popover'; +import TextField from '@material-ui/core/TextField'; +import Typography from '@material-ui/core/Typography'; +import withStyles from '@material-ui/core/styles/withStyles'; + +// Material UI Icons +import AddCircleIcon from '@material-ui/icons/AddBox'; +import CheckMarkIcon from '@material-ui/icons/Check'; +import ErrorIcon from '@material-ui/icons/ErrorOutline'; +import SendIcon from '@material-ui/icons/Send'; + +// Redux +import { connect } from 'react-redux'; +import { + getDirectMessages, + createNewDirectMessage, + getNewDirectMessages, + reloadDirectMessageChannels, + sendDirectMessage +} from '../redux/actions/dataActions'; + +const styles = { + pageContainer: { + minHeight: 'calc(100vh - 50px - 60px)' + }, + sidePadding: { + maxWidth: 350 + }, + dmList: { + width: 300, + marginLeft: 15 + }, + dmItemsUpper: { + marginBottom: 1, + // height: 'calc(100vh - 50px - 142px)', + minHeight: 100, + maxHeight: 'calc(100vh - 50px - 142px)', + overflow: "auto" + }, + dmItemsLower: { + + }, + dmItemUsernameSelected: { + fontSize: 20, + color: 'white' + }, + dmItemUsernameUnselected: { + fontSize: 20, + color: '#1da1f2' + }, + dmItemTimeSelected: { + color: '#D6D6D6', + fontSize: 12, + float: 'right', + marginRight: 5, + marginTop: 5 + }, + dmItemTimeUnselected: { + color: 'black', + fontSize: 12, + float: 'right', + marginRight: 5, + marginTop: 5 + }, + dmRecentMessageSelected: { + wordBreak: "break-all", + color: '#D6D6D6' + }, + dmRecentMessageUnselected: { + wordBreak: "break-all", + color: 'black' + }, + dmListItemContainer: { + height: 100 + }, + dmListLayoutContainer: { + height: "100%" + }, + dmListRecentMessage: { + marginLeft: 10, + marginRight: 10 + }, + dmListTextLayout: { + height: 30 + }, + dmCardUnselected: { + fontSize: 20, + backgroundColor: '#FFFFFF', + width: 300 + }, + dmCardSelected: { + fontSize: 20, + backgroundColor: '#1da1f2', + width: 300 + }, + messagesGrid: { + // // margin: "auto" + // height: "auto", + // width: "auto" + }, + messagesBox: { + width: 450 + }, + messagesContainer: { + height: 'calc(100vh - 50px - 110px)', + overflow: 'auto', + width: 450, + marginLeft: 2, + marginRight: 17 + }, + fromMessage: { + minWidth: 150, + maxWidth: 350, + minHeight: 40, + marginRight: 2, + marginTop: 2, + marginBottom: 10, + backgroundColor: '#008394', + color: '#FFFFFF', + float: 'right' + }, + toMessage: { + minWidth: 150, + maxWidth: 350, + minHeight: 40, + marginLeft: 15, + marginTop: 2, + marginBottom: 10, + backgroundColor: '#008394', + color: '#FFFFFF', + float: 'left' + }, + messageContent: { + // maxWidth: 330, + // width: 330, + wordBreak: "break-all", + textAlign: 'left', + marginLeft: 5, + marginRight: 5 + }, + messageTime: { + color: '#D6D6D6', + textAlign: 'left', + marginLeft: 5, + fontSize: 12 + }, + writeMessage: { + backgroundColor: '#FFFFFF', + boxShadow: '0px 0px 5px 0px grey', + width: 450 + }, + messageTextField: { + width: 388 + }, + messageButton: { + backgroundColor: '#1da1f2', + marginTop: 8, + marginLeft: 2 + }, + loadingUsernameChecks: { + height: 55, + width: 55, + marginLeft: 5 + }, + errorIcon: { + height: 55, + width: 55, + marginLeft: 5, + color: '#ff3d00' + }, + checkMarkIcon: { + height: 55, + width: 55, + marginLeft: 5, + color: '#1da1f2' + }, + createButton: { + // textAlign: "center", + // display: "block", + marginLeft: 96, + marginRight: 96, + position: "relative" + } +}; + +export class directMessages extends Component { + constructor() { + super(); + this.state = { + hasChannelSelected: false, + selectedChannel: null, + dmData: null, + anchorEl: null, + createDMUsername: '', + usernameValid: false, + // message: '', + drafts: {}, + errors: null + }; + } + + componentDidUpdate() { + if (this.state.hasChannelSelected) { + document.getElementById('messagesContainer').scrollTop = document.getElementById( + 'messagesContainer' + ).scrollHeight; + } + } + + componentDidMount() { + this.props.getDirectMessages(); + // this.updatePage(); + } + + // Updates the state whenever redux is updated + componentWillReceiveProps(nextProps) { + if (nextProps.directMessages && !_.isEqual(nextProps.directMessages, this.state.dmData)) { + this.setState({ dmData: nextProps.directMessages}, () => { + if (this.state.selectedChannel) { + this.state.dmData.forEach((channel) => { + if (channel.dmId === this.state.selectedChannel.dmId) { + this.setState({ + selectedChannel: channel + }); + } + }); + } + }); + } + } + + updatePage = async() => { + while (true) { + await this.sleep(15000); + // console.log("getting new DMs"); + this.props.getNewDirectMessages(); + } + } + + sleep = (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + // Handles selecting different DM channels + handleClickChannel = (event) => { + this.setState({ + hasChannelSelected: true + }); + + const dmItemsUpper = document.getElementById("dmItemsUpper"); + let target = event.target; + let dmChannelKey; + + // Determine which DM channel was clicked by finding the data-key. + // A while loop is necessary, because the user can click on any part of the + // DM list item. dmItemsUpper is the list container of the dmItems + while (target !== dmItemsUpper) { + dmChannelKey = target.dataset.key; + + if (dmChannelKey) { + break; + } else { + target = target.parentNode; + } + } + + // Save the entire DM channel in the state so that it is easier to load the messages + this.state.dmData.forEach((channel) => { + if (channel.dmId === dmChannelKey) { + this.setState({ + selectedChannel: channel + }); + } + }); + }; + + formatDateToString(dateString) { + let newDate = new Date(Date.parse(dateString)); + return newDate.toDateString(); + } + + formatDateToTimeDiff(dateString) { + return dayjs(dateString).fromNow(); + } + + shortenText = (text, length) => { + // Shorten the text + let shortened = text.slice(0, length + 1); + + // Trim whitespace from the end of the text + if (shortened[shortened.length - 1] === ' ') { + shortened = shortened.trimRight(); + } + + // Add ... to the end + shortened = `${shortened}...`; + + return shortened; + } + + handleOpenAddDMPopover = (event) => { + this.setState({ + anchorEl: event.currentTarget + }); + }; + + handleCloseAddDMPopover = () => { + this.setState({ + anchorEl: null, + createDMUsername: '', + usernameValid: false + }); + }; + + handleChangeAddDMUsername = (event) => { + this.setState({ + createDMUsername: event.target.value + }); + }; + + handleClickCreate = () => { + this.props.createNewDirectMessage(this.state.createDMUsername) + .then(() => { + return this.props.reloadDirectMessageChannels(); + }) + .then(() => { + this.handleCloseAddDMPopover(); + return; + }) + .catch(() => { + return; + }) + } + + handleChangeMessage = (event) => { + let drafts = this.state.drafts; + drafts[this.state.selectedChannel.dmId] = event.target.value; + this.setState({ + drafts + }); + } + + handleClickSend = () => { + // console.log(this.state.drafts[this.state.selectedChannel.dmId]); + let drafts = this.state.drafts; + if (this.state.hasChannelSelected && drafts[this.state.selectedChannel.dmId]) { + this.props.sendDirectMessage(this.state.selectedChannel.recipient, drafts[this.state.selectedChannel.dmId]); + drafts[this.state.selectedChannel.dmId] = null; + this.setState({ + drafts + }); + } + } + + render() { + const { classes, user: { credentials: { dmEnabled } } } = this.props; + const loadingDirectMessages = this.props.UI.loading2; + const creatingDirectMessage = this.props.UI.loading3; + const sendingDirectMessage = this.props.UI.loading4; + let errors = this.props.UI.errors ? this.props.UI.errors : {}; + dayjs.extend(relativeTime); + + // Used for the add button on the dmList + const open = Boolean(this.state.anchorEl); + const id = open ? 'simple-popover' : undefined; + + let dmListMarkup = this.state.dmData ? ( + this.state.dmData.map((channel) => ( + + + + + + + + + {channel.recipient} + + + + + {channel.recentMessageTimestamp ? ( + this.formatDateToTimeDiff(channel.recentMessageTimestamp) + ) : null} + + + + + + + { + !channel.recentMessage ? + 'No messages' + : + channel.recentMessage.length > 65 ? + this.shortenText(channel.recentMessage, 65) + : + channel.recentMessage + } + + + + + + )) + ) : ( +

You don't have any DMs yet

+ ) + + let messagesMarkup = + this.state.selectedChannel !== null ? this.state.selectedChannel.messages.length > 0 ? ( + this.state.selectedChannel.messages.map((messageObj) => ( + + + {messageObj.message} + + {this.formatDateToString(messageObj.createdAt)} + + + + )) + ) : ( +

No DMs here

+ ) : ( +

Select a DM channel

+ ); + + let addDMMarkup = ( +
+ + + + + + + + + + Who would you like to start a DM with? + + + + + + + + + + + + + + +
+ ); + + return ( + loadingDirectMessages ? : + (dmEnabled !== undefined && dmEnabled !== null && !dmEnabled ? Oops! It looks like you have DMs disabled. You can enable them on the Edit Profile page. : + + + + + + {dmListMarkup} + + + + + {addDMMarkup} + + + + + + + + {this.state.hasChannelSelected && ( + + + + {messagesMarkup} + + + + + + + { + sendingDirectMessage && + + // Won't accept classes style for some reason + } + + + + )} + {!this.state.hasChannelSelected && + this.state.dmData && Select a DM on the left} + + + + + ) + ); + } +} + +directMessages.propTypes = { + classes: PropTypes.object.isRequired, + getDirectMessages: PropTypes.func.isRequired, + createNewDirectMessage: PropTypes.func.isRequired, + getNewDirectMessages: PropTypes.func.isRequired, + reloadDirectMessageChannels: PropTypes.func.isRequired, + sendDirectMessage: PropTypes.func.isRequired, + user: PropTypes.object.isRequired, + UI: PropTypes.object.isRequired +}; + +const mapStateToProps = (state) => ({ + user: state.user, + UI: state.UI, + directMessages: state.data.directMessages +}); + +const mapActionsToProps = { + getDirectMessages, + createNewDirectMessage, + getNewDirectMessages, + reloadDirectMessageChannels, + sendDirectMessage +}; + +export default connect(mapStateToProps, mapActionsToProps)(withStyles(styles)(directMessages)); diff --git a/twistter-frontend/src/pages/editProfile.js b/twistter-frontend/src/pages/editProfile.js index 710636e..1d1e3cc 100644 --- a/twistter-frontend/src/pages/editProfile.js +++ b/twistter-frontend/src/pages/editProfile.js @@ -14,6 +14,8 @@ import Popover from "@material-ui/core/Popover"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; import withStyles from "@material-ui/core/styles/withStyles"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Switch from "@material-ui/core/Switch"; import IconButton from "@material-ui/core/IconButton"; import EditIcon from "@material-ui/icons/Edit"; import Tooltip from "@material-ui/core/Tooltip"; @@ -99,6 +101,7 @@ export class editProfile extends Component { email: res.data.email, handle: res.data.handle, bio: res.data.bio ? res.data.bio : "", + dmEnabled: res.data.dmEnabled === false ? false : true, pageLoading: false }); }) @@ -121,6 +124,8 @@ export class editProfile extends Component { email: "", handle: "", bio: "", + dmEnabled: false, + togglingDirectMessages: false, anchorEl: null, loading: false, pageLoading: false, @@ -183,6 +188,28 @@ export class editProfile extends Component { }); }; + handleDMSwitch = () => { + let enable; + + if (this.state.dmEnabled) { + enable = {enable: false}; + } else { + enable = {enable: true}; + } + + this.setState({ + dmEnabled: enable.enable, + togglingDirectMessages: true + }); + + axios.post("/dms/toggle", enable) + .then(() => { + this.setState({ + togglingDirectMessages: false + }); + }) + } + handleImageChange = (event) => { if (event.target.files[0]) { const image = event.target.files[0]; @@ -222,7 +249,6 @@ export class editProfile extends Component { const uploading = this.props.UI.loading; const { errors, loading } = this.state; -// <<<<<<< edit-profile-image-upload let imageMarkup = this.props.user.credentials.imageUrl ? ( + + } + label="Enable Direct Messages" + /> +