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

Feat/mentions #169

Merged
merged 4 commits into from
Dec 21, 2024
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
105 changes: 71 additions & 34 deletions app/src/components/ExpandingTextArea.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,96 @@
import { useEffect, useRef } from "react";
import styled from "styled-components";
import { MentionsInput, Mention } from "react-mentions";
import useMentionList from "@features/chats/hooks/useMentionList";

const Textarea = styled.textarea`
outline: none;
border: none;

flex: 1;
align-self: center;

caret-color: var(--accent-color);
const StyledMentionsInput = styled.div`
width: 100%;
max-width: 27rem;
color: var(--color-text);

resize: none;
overflow: hidden;

font-size: 1rem;
line-height: 1.5;
padding: 0.25rem;

max-height: 300px;
`;

type PropsType = {
input: string;
setInput: (value: string) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
onKeyDown?: (
e:
| React.KeyboardEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLTextAreaElement>
) => void;
};

function ExpandingTextArea({ input, setInput, onKeyDown }: PropsType) {
const ref = useRef<HTMLTextAreaElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);

const { filteredMembers } = useMentionList();

function handleInput() {
if (ref.current) {
ref.current.style.height = "auto";
ref.current.style.height = `${ref.current.scrollHeight}px`;
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
}
}

useEffect(() => {
if (ref.current) {
if (ref.current.value === "" || input === "") {
ref.current.style.height = "32px";
if (textareaRef.current) {
if (textareaRef.current.value === "" || input === "") {
textareaRef.current.style.height = "32px";
}
}
}, [input]);

console.log(filteredMembers);

return (
<Textarea
ref={ref}
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Message"
rows={1}
onInput={handleInput}
onKeyDown={onKeyDown}
/>
<StyledMentionsInput>
<MentionsInput
value={input}
onChange={(e: any) => setInput(e.target.value)}
onKeyDown={onKeyDown!}
onInput={handleInput}
placeholder="Message"
a11ySuggestionsListLabel="Suggested mentions"
style={{
input: {
overflow: "hidden",
border: "none",
forcedColorAdjust: "none",
color: "var(--color-text)",
outline: "none"
},
suggestions: {
list: {
backgroundColor: "var(--color-background)",
border: "none",
borderRadius: "0.5rem",
position: "absolute",
zIndex: 1,
bottom: "2rem",
width: "20rem",
boxShadow: "0 2px 5px rgba(0, 0, 0, 0.2)"
},
item: {
padding: "0.5rem 1rem",
"&focused": {
backgroundColor: "var(--accent-color)",
color: "white"
}
}
}
}}
>
<Mention
trigger="@"
data={filteredMembers}
displayTransform={(display) => `@${display}`}
style={{
backgroundColor: "transparent",
color: "ButtonFace",
border: "none"
}}
/>
</MentionsInput>
</StyledMentionsInput>
);
}

Expand Down
6 changes: 5 additions & 1 deletion app/src/data/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ enum icons {
Eye,
Sun,
Group,
CallAccept
CallAccept,
Mention,
}

type iconStrings = keyof typeof icons;
Expand Down Expand Up @@ -442,6 +443,9 @@ const iconImports: Record<iconStrings, IconConfig> = {
importFn: () => import("@mui/icons-material/Visibility"),
defaultProps: { fontSize: "small" },
},
Mention:{
importFn: () => import ("@mui/icons-material/AlternateEmail"),
},
};

const iconCache = new Map<string, React.ReactElement>();
Expand Down
27 changes: 26 additions & 1 deletion app/src/features/chats/ChatBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { getChatByID } from "./utils/helpers";
import { useParams } from "react-router-dom";
import { useAppSelector } from "@hooks/useGlobalState";
import MessageProvider from "./contexts/MessageProvider";
import { getIcon } from "@data/icons";
import useTraversalMentions from "./hooks/useTraverseMentions";
import Icon from "@components/Icon";

const ScrollContainer = styled.div`
width: 100%;
height: 82dvh;
overflow-y: auto;
position: relative;
margin-top: 3rem;
&::-webkit-scrollbar {
width: 5px;
Expand Down Expand Up @@ -45,6 +47,9 @@ function ChatBody() {
const { inView, ref } = useInView({ threshold: 0.01 });
const scrollContainerRef = useRef<HTMLDivElement | null>(null);

const { handleNextMentionMessage, mentionMessages } = useTraversalMentions();


useEffect(() => {
if (inView && hasNextPage && chatId) {
const container = scrollContainerRef.current;
Expand Down Expand Up @@ -103,6 +108,26 @@ function ChatBody() {
</MessageProvider>
);
})}
<Icon onClick={handleNextMentionMessage} data-testid="mention-icon">
{mentionMessages.length > 0 &&
getIcon("Mention", {
sx: {
fontSize: "2.5rem",
color: "var(--accent-color)",
backgroundColor: `var(--color-background)`,
borderRadius: "50%",
position: "absolute",
bottom: "8rem",
right: "3%",
cursor: "pointer",
":hover": {
backgroundColor: "var(--color-background-own-1)",
color: "white",
transition: "0.2s",
},
},
})}
</Icon>
</ScrollContainer>
);
}
Expand Down
8 changes: 6 additions & 2 deletions app/src/features/chats/ChatInputIcons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getIcon } from "@data/icons";
import styled from "styled-components";
import MediaUploadComponent from "./media/MediaUploadComponent";
import Icon from "@components/Icon";
import { useContext } from "react";
import React, { useContext } from "react";
import { ChatInputContext } from "./ChatBox";

const InvisibleButton = styled.div`
Expand All @@ -27,7 +27,11 @@ function ChatInputIcons() {
setIsEmojiSelectorOpen((show: boolean) => !show);
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const handleKeyDown = (
e:
| React.KeyboardEvent<HTMLInputElement>
| React.KeyboardEvent<HTMLTextAreaElement>
) => {
setIsEmojiSelectorOpen(false);
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
Expand Down
4 changes: 3 additions & 1 deletion app/src/features/chats/ChatItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import styled from "styled-components";

import Avatar from "@components/Avatar";
import { DetailedChatInterface } from "@state/messages/chats";
import RenderWithMention from "@utils/renderWithMentions";

const Container = styled.li<{ $active?: boolean }>`
display: flex;
Expand Down Expand Up @@ -64,7 +65,8 @@ const ChatItem = ({
const navigate = useNavigate();

const timestamp = lastMessage?.timestamp || "No messages";
const lastMessageContent = lastMessage?.content || "No messages";
const lastMessageContent =
RenderWithMention(lastMessage?.content!, lastMessage?._id!) || "No messages";

const { chatId } = useParams<{ chatId: string }>();

Expand Down
3 changes: 0 additions & 3 deletions app/src/features/chats/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import MessageContent from "./MessageContent";
import useCheckBox from "@features/forward/hooks/useCheckBox";
import MessageDetails from "./MessageDetails";

import useScrollToLastMsg from "./hooks/useScrollToLastMsg";
import useHover from "./hooks/useHover";
import { useMessageContext } from "./contexts/MessageProvider";
import React from "react";
Expand Down Expand Up @@ -75,7 +74,6 @@ const CheckBoxWrapper = styled.div`
const Message = React.memo(() => {
const { _id: id, chatId, isMine, chatType } = useMessageContext();

const { lastMessageRef } = useScrollToLastMsg();
useScrollToSearchResultsMsg();

const { isChecked, toggleCheckBox, showCheckBox } = useCheckBox({
Expand All @@ -94,7 +92,6 @@ const Message = React.memo(() => {
)}

<StyledMessage
ref={lastMessageRef}
key={id}
$isMine={chatType === "channel" ? false : isMine}
data-message-id={id}
Expand Down
2 changes: 1 addition & 1 deletion app/src/features/chats/SenderName.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useParams } from "react-router-dom";
import styled from "styled-components";

import { useAppSelector } from "@hooks/useGlobalState";
import { useChatMembers } from "./hooks/useChatMember";
import { useChatMembers } from "./hooks/useChatMembers";

import { useMessageContext } from "./contexts/MessageProvider";

Expand Down
2 changes: 1 addition & 1 deletion app/src/features/chats/Topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { setChatIsBlocked } from "@state/messages/chats";

import { useSocket } from "@hooks/useSocket";
import { useAppDispatch, useAppSelector } from "@hooks/useGlobalState";
import { useChatMembers } from "./hooks/useChatMember";
import { useChatMembers } from "./hooks/useChatMembers";
import { useBlock } from "@features/privacy-settings/hooks/useBlock";
import { useRightSideBarContext } from "@features/groups/contexts/RightSideBarProvider";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export function useChatMembers(memberIDs?: ChatMember[]): Member[] {
members.find((member) => member._id === filteredID._id)
)
.filter((member): member is Member => member !== undefined); // Ensure no undefined values

}
1 change: 1 addition & 0 deletions app/src/features/chats/hooks/useChats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function useChats() {

useEffect(() => {
const processMessages = async ({ chats, members }: ChatsState) => {
if (!chats) return;
await Promise.all(
chats.map(async (chat) => {
if (chat.type === "private") {
Expand Down
5 changes: 5 additions & 0 deletions app/src/features/chats/hooks/useFetchNextPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ function useFetchNextPage() {

if (data) {
const lastFetchedPage = data.pages[data.pages.length - 1];
lastFetchedPage.messages.forEach((element: any) => {
element.isMention = false;
element.isSeen = false;
});

if (lastFetchedPage && chatId) {
processMessages(lastFetchedPage.messages).then(() => {
dispatch(
Expand Down
20 changes: 20 additions & 0 deletions app/src/features/chats/hooks/useMentionList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useAppSelector } from "@hooks/useGlobalState";
import { useParams } from "react-router-dom";
import { getChatByID } from "../utils/helpers";
import { useChatMembers } from "./useChatMembers";

function useMentionList() {
const { chatId } = useParams<{ chatId: string }>();
const chats = useAppSelector((state) => state.chats.chats);
const chat = getChatByID({ chats: chats, chatID: chatId! });

const chatMembers = useChatMembers(chat?.members);
const filteredMembers = chatMembers.map((member) => ({
id: member.username,
display: `${member.screenFirstName} ${member.screenLastName}`,
}));

return { filteredMembers };
}

export default useMentionList;
4 changes: 3 additions & 1 deletion app/src/features/chats/hooks/useMessageSender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ export const useMessageSender = () => {
isReply,
status: MessageStatus.sent,
media: file,
threadMessages: []
threadMessages: [],
isMention: false,
isSeen: false,
};

const threadMessage = {
Expand Down
Loading
Loading