Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Message Threads #12

Merged
merged 17 commits into from
Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ yarn start
- [x] Main Chat Window Functionalities (Send/Delete/Edit/...etc) Messages
- [x] User Status Feature
- [x] Chat Window Attachments (Images/Videos/Audios/Quotes)
- [x] Parsing Message Threads (Images/Videos/Audios/Quotes)

### Screenshots

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-icons": "^4.4.0",
"react-redux": "^8.0.2",
"react-redux": "^7.2.8",
"react-router-dom": "^6.3.0",
"redux": "^4.2.0",
"redux-thunk": "^2.4.1",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"styled-components": "^5.3.5",
Expand Down
10 changes: 6 additions & 4 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import React, {useEffect} from "react";
import React from "react";
import { hot } from "react-hot-loader/root";
import { HashRouter as Router, Routes, Route } from "react-router-dom";
import { Provider } from "react-redux";
import Login from './Login/Login'
import ChatList from "./ChatsList/ChatList";
import ChatWindow from "./ChatWindow/ChatWindow";
import store from "../state/store";
import "./style.css";

function App() {
return (
<div>
<Provider store={store}>
<Router>
<Routes>
<Route path='/' exact element={<Login/>}/>
<Route path="/list" element={<ChatList />} />
<Route path="/chat/:id" element={<ChatWindow />} />
<Route path="/chat/:id" element={<ChatWindow isThread={false} />} />
</Routes>
</Router>
</div>
</Provider>
);
}

Expand Down
61 changes: 41 additions & 20 deletions src/components/ChatWindow/ChatWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ import { realTimeLoginWithAuthToken } from "../../util/login.util";
import Header from "./Header/Header";
import MessageForm from "./MessageForm/MessageForm";
import MessageList from "./MessageList/MessageList";
import { loadMessagesFromRoom, realTimeSubscribeToRoom, markRoomAsRead } from "../../util/chatsWindow.util";
import { loadMessagesFromRoom, realTimeSubscribeToRoom, markRoomAsRead, loadMessagesFromThread } from "../../util/chatsWindow.util";
import { RealtimeAPIMessage } from "../../interfaces/message";
import { DDPMessage } from "../../interfaces/sdk";
import styled from "styled-components"
import { MESSAGES_LOAD_PER_REQUEST } from "../../constants";
import { useSelector } from "react-redux";

const Container = styled.div`
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
overflow: unset;
`

const HeaderFooterContainer = styled.div`
Expand All @@ -28,19 +28,30 @@ interface MessagesMap {
[_id: string]: RealtimeAPIMessage
}

/**
* @returns if element is in the view of user
*/
const isInViewport = (element: HTMLElement | null, offset = 0) => {
if (!element) return false;
const top = element.getBoundingClientRect().top;
return (top + offset) >= 0 && (top - offset) <= window.innerHeight;
}

function ChatWindow() {
const { id } = useParams();
/**
*
* @param props.isThread returns true if view should be thread
* @returns Either Complete Chat Window (Like Channel) or Thread View
*/

function ChatWindow(props: {isThread: boolean}) {
const id = useParams().id;
const bottomRef = useRef<null | HTMLElement>(null);
const [messages, setMessages] = useState<MessagesMap>({});
const [messageToEdit, setMessageToEdit] = useState<RealtimeAPIMessage | null>(null);
const [loaded, setLoaded] = useState<boolean>(false);

const thread = useSelector((state: any) => state.thread);
const tmid = thread.tmid;

const loginToRoom = async () => {
await realTimeLoginWithAuthToken();
Expand All @@ -50,12 +61,15 @@ function ChatWindow() {
setLoaded(true);
}



const loginToThread = async () => {
await realTimeSubscribe();
await showMessages();
setLoaded(true);
}

const processMessages = async(ddpMessage:DDPMessage) => {
const message = ddpMessage.fields.args[0];

const message: RealtimeAPIMessage = ddpMessage.fields.args[0];
if(message.tmid && !props.isThread || message.tmid != tmid) return;
let scroll: boolean = false;
// Check if user is already at down of page then scroll to show message
if(isInViewport(bottomRef.current)){
Expand All @@ -64,7 +78,7 @@ function ChatWindow() {

await addMessage(message);
if(scroll) bottomRef.current?.scrollIntoView({behavior: 'smooth'});
await markRoomAsRead(id);
if(props.isThread) await markRoomAsRead(id);
}

const realTimeSubscribe = async () => {
Expand All @@ -78,8 +92,14 @@ function ChatWindow() {
lastMessageDate = messages[ messagesKeys[0] ].ts;
}

const newMessagesResponse: {messages: RealtimeAPIMessage[], unreadNotLoaded: number} = await loadMessagesFromRoom(id, MESSAGES_LOAD_PER_REQUEST, lastMessageDate);
const newMessages = newMessagesResponse.messages;
let newMessages: RealtimeAPIMessage[];

if(props.isThread){
newMessages = await loadMessagesFromThread(tmid);
} else {
const newMessagesResponse: {messages: RealtimeAPIMessage[]} = await loadMessagesFromRoom(id, MESSAGES_LOAD_PER_REQUEST, lastMessageDate);
newMessages = newMessagesResponse.messages;
}

setMessages((oldMessages) => {
let toBeMessages: MessagesMap = {};
Expand Down Expand Up @@ -108,26 +128,27 @@ function ChatWindow() {
}

useEffect(() => {
loginToRoom();
if(!props.isThread) loginToRoom();
else loginToThread();
}, []);


return loaded && (
<Container>
<HeaderFooterContainer>
return loaded ? (
<Container >
{!props.isThread && <HeaderFooterContainer>
<Header />
</HeaderFooterContainer>
</HeaderFooterContainer>}
<MessageList messages={messages}
loadMoreMessages={loadMoreMessages}
onEditMessageAction={onEditMessageAction}
setMessageToEdit={setMessageToEdit}
isThread={props.isThread && tmid}
/>
<div ref={bottomRef} />
<HeaderFooterContainer>
<MessageForm messageToEdit={messageToEdit} setMessageToEdit={setMessageToEdit} />
<MessageForm messageToEdit={messageToEdit} setMessageToEdit={setMessageToEdit} isThread={props.isThread && tmid} />
</HeaderFooterContainer>
</Container>
);
) : null;
}

export default hot(ChatWindow);
4 changes: 2 additions & 2 deletions src/components/ChatWindow/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const Container = styled.div`
border-bottom:1px solid #eeeff1;
padding:8px 0px 8px 15px;
height:2.5rem;
overflow: hidden;
overflow-x: hidden;
background-color: #FFF;
z-index: 1000;
`
Expand Down Expand Up @@ -60,7 +60,7 @@ const RoomName = styled.div`
font-weight: 700;
letter-spacing: 0rem;
line-height: 1.5rem;
overflow: hidden;
overflow-x: hidden;
margin-inline: 0.25rem;
`

Expand Down
6 changes: 5 additions & 1 deletion src/components/ChatWindow/MessageForm/MessageForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import styled from "styled-components"
import { sendTextMessage, editTextMessage } from "../../../util/message.util";
import { useParams } from "react-router-dom";
import { RealtimeAPIMessage } from '../../../interfaces/message';
import { useSelector } from "react-redux";

const Container = styled.div`
position: fixed;
Expand Down Expand Up @@ -31,6 +32,8 @@ const TextInput = styled.textarea`
function MessageForm(props: any) {
const { id: roomId } = useParams();
const [message, setMessage] = useState("");
const thread = useSelector((state: any) => state.thread);
const tmid = thread.tmid;

const onMessageChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
setMessage(e.target.value);
Expand All @@ -48,7 +51,8 @@ function MessageForm(props: any) {
const sendMessage = async () => {
// Send New Message
if(!props.messageToEdit){
return await sendTextMessage(roomId, message);
const threadId = props.isThread ? tmid : null;
return await sendTextMessage(roomId, message, threadId);
}

// Edit Existing Message
Expand Down
57 changes: 44 additions & 13 deletions src/components/ChatWindow/MessageList/MessageList.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import React from "react";
import { hot } from "react-hot-loader/root";
import styled from "styled-components";
import MessageRow from './MessageRow/MessageRow';
import { RealtimeAPIMessage } from '../../../interfaces/message';
import MessageRow from "./MessageRow/MessageRow";
import MessageThread from "../MessageThread/MessageThread";
import { useSelector } from "react-redux";

const Container = styled.div`
overflow: hidden;
display: flex;
flex-direction: row;
height: 100%;
overflow-x: hidden;
`

const ChatContainer = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
overflow-y: auto;
overflow: auto;
flex: 20;
height: 100%;
width: 100%;
overflow-x: hidden;
`
const ShowMoreMessages = styled.div`
width: 100%;
Expand All @@ -18,25 +30,44 @@ const ShowMoreMessages = styled.div`
font-size: 20px;
height: 30px;
cursor: pointer;
margin-bottom: 5px;
&:hover {
background-color:rgba(186, 186, 186, 0.1);
}
`


const DarkLayer = styled.div`
${(props:{addBlackLayer: boolean}) => {
if(props.addBlackLayer){
return `
background-color: rgba(0, 0, 0, .6);
height: 100%;
width: 100%;
position: fixed;
z-index: 999;
`
}
}}
`
function MessageList(props : any) {

const messages = props.messages;
const state = useSelector((state: any) => state.thread);

return (
<Container>
<ShowMoreMessages onClick={props.loadMoreMessages}>
Show More Messages
</ShowMoreMessages>
{messages ? Object.keys(messages).map((messageId: string) => {
return (
<MessageRow key={messageId} message={messages[messageId]} onEditMessageAction={props.onEditMessageAction} setMessageToEdit={props.setMessageToEdit} />
);
}) : null}
<ChatContainer>
<DarkLayer addBlackLayer={!props.isThread && state.tmid}/>
<ShowMoreMessages onClick={props.loadMoreMessages}>
Show More Messages
</ShowMoreMessages>
{messages ? Object.keys(messages).map((messageId: string) => {
return (
<MessageRow key={messageId} message={messages[messageId]} onEditMessageAction={props.onEditMessageAction} setMessageToEdit={props.setMessageToEdit} />
);
}) : null}
</ChatContainer>
{!props.isThread && state.tmid ? <MessageThread /> : null}
</Container>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import ParseOtherMessageTypes from "./components/MessageBodyRender/MessageType";
import MessageActions from "./components/MessageActions/MessageActions";
import { deleteMessageById } from "../../../../util/message.util";
import ProfileImage from "../../../main/ProfileImage/ProfileImage";
import MessageThread from "./components/Threads/Threads";

const MessageContainer = styled.div`
width:100%;
display: flex;
flex-direction: row;
justify-content: normal;
align-items: start;
padding: 5px 10px;
Expand All @@ -21,6 +23,9 @@ const MessageContainer = styled.div`
background-color: ${(props: { isEditing: boolean; }) => !props.isEditing ? "rgba(186, 186, 186, 0.1)" : "#fff6d6"};
}
background-color: ${(props: { isEditing: boolean; }) => !props.isEditing ? "transparent" : "#fff6d6"};
&:nth-child(3) {
margin-top: 15px;
}
`

const BodyContainer = styled.div`
Expand Down Expand Up @@ -115,6 +120,7 @@ function MessageRow(props : any) {
<MessageActions
onMessageDelete = {deleteMessage}
onMessageEdit = {editMessage}
id = {message._id}
show={isShowActionsModal}
/>
</MessageInfo>
Expand All @@ -128,6 +134,7 @@ function MessageRow(props : any) {
/>
</MessageBody>
{message.attachments && <Attachments attachments={message.attachments} file={message.file} />}
{message.tcount && <MessageThread message={message} />}
</BodyContainer>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React, { useState } from "react";
import { hot } from "react-hot-loader/root";
import styled from "styled-components";
import { BsThreeDotsVertical } from "react-icons/bs";
import { useDispatch } from "react-redux";
import { openThread } from "../../../../../../state/actions";

const Container = styled.div`
position: absolute;
Expand Down Expand Up @@ -33,6 +35,7 @@ const ActionsLi = styled.li`

function MessageActions(props : any) {
const [isModalOpen, setModal] = useState(false);
const dispatch = useDispatch();

const toggleActionsModal = () => {
setModal(!isModalOpen);
Expand All @@ -48,6 +51,10 @@ function MessageActions(props : any) {
toggleActionsModal();
}

const onReplyInThread = () => {
dispatch(openThread(props.id));
}


return props.show && (
<Container>
Expand All @@ -59,6 +66,7 @@ function MessageActions(props : any) {
<ActionsUl style={{listStyleType:"none"}}>
<ActionsLi onClick={onMessageEdit}>Edit Message</ActionsLi>
<ActionsLi onClick={onMessageDelete}>Delete Message</ActionsLi>
<ActionsLi onClick={onReplyInThread}>Reply In Thread</ActionsLi>
</ActionsUl>
</ActionsModal>
): null}
Expand Down
Loading