Skip to content

Commit

Permalink
SK-194 implemented typing status indicator (#126)
Browse files Browse the repository at this point in the history
* SK-194 added names view

* SK-194 added support name in last message view

* SK-194 added validation for system message

* SK-194 updated last message structure

* SK-194 fixed error on close chat info

* SK-194 minor fixed

* SK-194 fixed user name view, cut if length > 8

* SK-194 fixed last message user name view (nowrap)
  • Loading branch information
Oleksandr1414 authored Jul 23, 2024
1 parent 3cfc2bb commit 3ecf02d
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 19 deletions.
31 changes: 20 additions & 11 deletions src/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Api {
this.onMessageListener = null;
this.onMessageStatusListener = null;
this.onUserActivityListener = null;
this.onUserTypingListener = null;
this.onConversationCreateListener = null;
this.onConversationUpdateListener = null;
this.onConversationDeleteListener = null;
Expand All @@ -34,6 +35,13 @@ class Api {
const message = JSON.parse(e.data);
console.log("[socket.message]", message);

if (message.typing) {
if (this.onUserTypingListener) {
this.onUserTypingListener(message.typing);
}
return;
}

if (message.system_message) {
const {
x: {
Expand Down Expand Up @@ -435,17 +443,18 @@ class Api {
return this.sendPromise(requestData, resObjKey);
}

async statusTyping(data) {
//===============to do
const requestData = {
typing: {
id: "xyz",
type: "start",
cid: "currentConversationId",
},
};
const resObjKey = "success";
return this.sendPromise(requestData, resObjKey);
async sendTypingStatus(data) {
return new Promise((resolve, reject) => {
const requestData = {
typing: {
cid: data.cid,
},
};

this.responsesPromises[requestData.typing.id] = { resolve, reject };
this.socket.send(JSON.stringify(requestData));
console.log("[socket.send]", requestData);
});
}

async conversationCreate(data) {
Expand Down
16 changes: 16 additions & 0 deletions src/components/_helpers/DotsLoader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ThreeDots } from "react-loader-spinner";

export default function DotsLoader({ width, height, mainColor }) {
return (
<ThreeDots
visible={true}
height={width}
width={height}
color={mainColor || " var(--color-accent-dark)"}
radius="9"
ariaLabel="three-dots-loading"
wrapperStyle={{}}
wrapperClass={"typing-dots"}
/>
);
}
39 changes: 39 additions & 0 deletions src/components/_helpers/TypingLine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import DotsLoader from "./DotsLoader";
import getLastMessageUserName from "@utils/user/get_last_message_user_name";
import { selectParticipantsEntities } from "@src/store/values/Participants";
import { useMemo } from "react";
import { useSelector } from "react-redux";

export default function TypingLine({ userIds, displayUserNames = false }) {
console.log(userIds);
const participants = useSelector(selectParticipantsEntities);

const usersNameView = useMemo(() => {
if (!displayUserNames) {
return null;
}

const typingUsers = userIds?.map((id) => participants[id]);
const typingUsersLength = typingUsers?.length;

if (typingUsersLength > 2) {
return `${getLastMessageUserName(typingUsers[0])} and ${
typingUsersLength - 1
} more `;
}

return typingUsers?.map(
(user, i) =>
`${getLastMessageUserName(user)}${
i !== typingUsersLength - 1 ? ", " : " "
}`
);
}, [participants, userIds, displayUserNames]);

return (
<div className="typing-line__container">
<DotsLoader height={22} width={16} />
<p>{usersNameView}typing</p>
</div>
);
}
10 changes: 10 additions & 0 deletions src/components/hub/chatForm/ChatFormHeader.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import TypingLine from "@components/_helpers/TypingLine";
import addSuffix from "@utils/navigation/add_suffix";
import getLastVisitTime from "@utils/user/get_last_visit_time";
import getUserFullName from "@utils/user/get_user_full_name";
Expand Down Expand Up @@ -70,6 +71,15 @@ export default function ChatFormHeader({ closeFormFunc }) {
}, [selectedConversation, participants, opponentId]);

const viewStatusActivity = useMemo(() => {
if (selectedConversation.typing_users?.length) {
return (
<TypingLine
userIds={selectedConversation.typing_users}
displayUserNames={selectedConversation.type === "g"}
/>
);
}

if (selectedConversation.type === "u") {
if (!isOpponentExist) {
return null;
Expand Down
28 changes: 22 additions & 6 deletions src/components/hub/elements/ConversationItem.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import LastMessage from "@components/message/LastMessage";
import TypingLine from "@components/_helpers/TypingLine";
import DynamicAvatar from "@components/info/elements/DynamicAvatar";
import getLastUpdateTime from "@utils/conversation/get_last_update_time";
import { useMemo } from "react";
Expand All @@ -14,8 +15,15 @@ export default function ConversationItem({
currentUserId,
chatAvatarUrl,
chatAvatarBlutHash,
lastMessageUserName,
}) {
const { updated_at, unread_messages_count, type, last_message } = chatObject;
const {
updated_at,
unread_messages_count,
type,
last_message,
typing_users,
} = chatObject;

const tView = useMemo(() => {
return getLastUpdateTime(updated_at, last_message);
Expand Down Expand Up @@ -50,11 +58,19 @@ export default function ConversationItem({
<div className="content-top__time">{tView}</div>
</div>
<div className="content-bottom">
<LastMessage
message={last_message}
count={unread_messages_count}
userId={currentUserId}
/>
{typing_users?.length && !isSelected ? (
<TypingLine
userIds={typing_users}
displayUserNames={type === "g"}
/>
) : (
<LastMessage
message={last_message}
viewName={lastMessageUserName}
count={unread_messages_count}
userId={currentUserId}
/>
)}
</div>
</div>
</div>
Expand Down
11 changes: 11 additions & 0 deletions src/components/hub/elements/ConversationItemList.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ConversationItem from "@components/hub/elements/ConversationItem";
import getLastMessageUserName from "@utils/user/get_last_message_user_name";
import getUserFullName from "@utils/user/get_user_full_name";
import navigateTo from "@utils/navigation/navigate_to";
import { getConverastionById } from "@store/values/Conversations";
Expand Down Expand Up @@ -27,12 +28,22 @@ export default function ConversationItemList({ conversations }) {
const chatParticipant = participants[chatParticipantId] || {};
const chatName = obj.name || getUserFullName(chatParticipant);

const lastMessageUserId = obj?.last_message?.from;
const lastMessageUser = participants[lastMessageUserId] || {};
const lastMessageUserName =
currentUserId === lastMessageUserId
? "You"
: getLastMessageUserName(lastMessageUser);

return (
<ConversationItem
key={obj._id}
isSelected={isSelected}
onClickFunc={() => convItemOnClickFunc(obj._id)}
chatName={chatName}
lastMessageUserName={
obj.type === "g" && !obj.last_message?.x ? lastMessageUserName : null
}
chatAvatarUrl={
obj.type === "g" ? obj.image_url : chatParticipant.avatar_url
}
Expand Down
13 changes: 13 additions & 0 deletions src/components/hub/elements/MessageInput.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import TextAreaInput from "@components/hub/elements/TextAreaInput";
import addSuffix from "@utils/navigation/add_suffix";
import api from "@api/api";
import isMobile from "@utils/get_device_type";
import { KEY_CODES } from "@helpers/keyCodes";
import { getSelectedConversationId } from "@store/values/SelectedConversation";
import { useEffect, useMemo, useState } from "react";
import { useLocation } from "react-router-dom";
import { useSelector } from "react-redux";

import { ReactComponent as Attach } from "@icons/options/Attach.svg";
import { ReactComponent as Send } from "@icons/options/Send.svg";
Expand All @@ -16,9 +19,19 @@ export default function MessageInput({
}) {
const location = useLocation();

const selectedConversationId = useSelector(getSelectedConversationId);

const [isPrevLocationAttach, setIsPrevLocationAttach] = useState(false);

let lastTypingRequestTime = null;
const handleInput = (e) => {
if (e.target.value.length > 0) {
if (new Date() - lastTypingRequestTime > 3000 || !lastTypingRequestTime) {
api.sendTypingStatus({ cid: selectedConversationId });
lastTypingRequestTime = new Date();
}
}

if (inputTextRef.current) {
const countOfLines = e.target.value.split("\n").length - 1;
inputTextRef.current.style.height = `${
Expand Down
2 changes: 1 addition & 1 deletion src/components/info/ChatInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default function ChatInfo() {
);

const participantsList = useMemo(() => {
if (!selectedConversation.participants || !currentUserId) {
if (!selectedConversation?.participants || !currentUserId) {
return null;
}

Expand Down
6 changes: 5 additions & 1 deletion src/components/message/LastMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import LastMessageMedia from "@components/message/lastMessage/LastMessageMedia";
import LastMessageStatus from "@components/message/lastMessage/LastMessageStatus";
import getFileType from "@utils/media/get_file_type";

export default function LastMessage({ message, count, userId }) {
export default function LastMessage({ message, count, userId, viewName }) {
if (!message) {
return null;
}
Expand All @@ -15,12 +15,16 @@ export default function LastMessage({ message, count, userId }) {
return "";
}
const fileType = att && getFileType(att.file_name);

return text || fileType || "";
};

return (
<>
<div className="content-bottom__last-message">
<p className="last-message__uname">
{viewName ? `${viewName}:` : null}
</p>
{lastAtt ? <LastMessageMedia attachment={lastAtt} /> : null}
<p className="last-message__text">{lastMessageText(body, lastAtt)}</p>
</div>
Expand Down
38 changes: 38 additions & 0 deletions src/services/messagesService.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {

class MessagesService {
currentChatId;
typingTimers = {};

constructor() {
api.onMessageStatusListener = (message) => {
Expand All @@ -48,6 +49,7 @@ class MessagesService {
const userInfo = jwtDecode(localStorage.getItem("sessionId"));
message.from === userInfo._id && (message["status"] = "sent");
store.dispatch(addMessage(message));
store.dispatch(upsertChat({ _id: message.cid, typing_users: null }));

let countOfNewMessages = 0;
message.cid === this.currentChatId && document.hasFocus()
Expand Down Expand Up @@ -87,6 +89,42 @@ class MessagesService {
}
};

api.onUserTypingListener = (data) => {
const { cid, from } = data;

const conversation = store.getState().conversations.entities[cid];
const newTypingUsersArray = [...(conversation.typing_users || [])];
!newTypingUsersArray.includes(from) && newTypingUsersArray.push(from);
store.dispatch(
upsertChat({
_id: cid,
typing_users: newTypingUsersArray,
})
);

const { clearTypingStatus, lastRequestTime } =
this.typingTimers[cid] || {};

if (new Date() - lastRequestTime > 3000 && clearTypingStatus) {
clearTimeout(clearTypingStatus);
}

this.typingTimers[cid] = {
clearTypingStatus: setTimeout(() => {
const conversation = store.getState().conversations.entities[cid];
store.dispatch(
upsertChat({
_id: cid,
typing_users: conversation.typing_users?.filter(
(id) => id !== from
),
})
);
}, 4000),
lastRequestTime: new Date(),
};
};

store.subscribe(() => {
let previousValue = this.currentChatId;
this.currentChatId = store.getState().selectedConversation.value.id;
Expand Down
11 changes: 11 additions & 0 deletions src/styles/GlobalParam.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ main {
padding-left: 30px;
}

.typing-line__container {
margin-left: 5px;

display: flex;
gap: 10px;
align-items: flex-end;
}
.typing-line__container > p {
color: var(--color-accent-dark);
}

.connect-line {
position: absolute;
top: 0;
Expand Down
5 changes: 5 additions & 0 deletions src/styles/hub/ChatList.css
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@
text-overflow: ellipsis;
white-space: nowrap;
}
.last-message__uname {
text-wrap: nowrap;
color: var(--color-accent-dark);
font-weight: 300;
}

.last-message__media {
width: auto;
Expand Down
17 changes: 17 additions & 0 deletions src/utils/user/get_last_message_user_name.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const cut = (text) => (text.length > 8 ? text.slice(0, 6) + "..." : text);

export default function getLastMessageUserName(userObject) {
if (!userObject) {
return null;
}

const { first_name, last_name, login } = userObject;

if (!first_name && !last_name) {
return login ? cut(login[0].toUpperCase() + login.slice(1)) : undefined;
}

const fullName = [first_name, last_name].filter(Boolean).join(" ");

return last_name && fullName.length <= 10 ? fullName : cut(first_name);
}

0 comments on commit 3ecf02d

Please sign in to comment.