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

SK-194 implemented typing status indicator #126

Merged
merged 9 commits into from
Jul 23, 2024
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"}
/>
);
}
32 changes: 32 additions & 0 deletions src/components/_helpers/TypingLine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import DotsLoader from "./DotsLoader";
import getUserFullName from "@utils/user/get_user_full_name";
import { selectParticipantsEntities } from "@src/store/values/Participants";
import { useMemo } from "react";
import { useSelector } from "react-redux";

export default function TypingLine({ userIds, isViewUserNames = false }) {
const participants = useSelector(selectParticipantsEntities);

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

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

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

return (
<div className="typing-line__container">
<DotsLoader height={22} width={16} />
<p>{isViewUserNames ? 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}
isViewUserNames={selectedConversation.type === "g"}
/>
);
}

if (selectedConversation.type === "u") {
if (!isOpponentExist) {
return null;
Expand Down
25 changes: 19 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,16 @@ 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 && !isSelected ? (
<TypingLine isViewUserNames={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
4 changes: 4 additions & 0 deletions src/styles/hub/ChatList.css
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@
text-overflow: ellipsis;
white-space: nowrap;
}
.last-message__uname {
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 @@
export default function getLastMessageUserName(userObject) {
if (!userObject) {
return null;
}

const cut = (text) => (text.length > 10 ? text.slice(0, 7) + "..." : text);

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);
}