Skip to content

Commit

Permalink
Merge pull request #1591 from ecency/feature/reply-to-message
Browse files Browse the repository at this point in the history
Feature/reply to message
  • Loading branch information
feruzm authored Apr 29, 2024
2 parents b542b86 + f602dd3 commit aaf957d
Show file tree
Hide file tree
Showing 29 changed files with 586 additions and 201 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function ChatsChannelMessages({ publicMessages, currentChannel, isPage }:
<>
<div className="channel-messages" ref={channelMessagesRef}>
{groupedMessages.map(([date, group], i) => (
<React.Fragment key={date.getTime()}>
<div className="relative" key={date.getTime()}>
{(i > 0 ? differenceInCalendarDays(date, groupedMessages[i - 1][0]) : 1) ? (
<ChatFloatingDate key={date.getTime()} currentDate={date} isPage={isPage} />
) : (
Expand Down Expand Up @@ -154,7 +154,7 @@ export function ChatsChannelMessages({ publicMessages, currentChannel, isPage }:
</Dropdown>
</>
))}
</React.Fragment>
</div>
))}

<ForwardMessageDialog
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function ChatFloatingDate({
"top-[180px] md:top-[57px]": isPage
})}
>
<div className="bg-gray-200 my-3 dark:bg-gray-800 rounded-full p-1 max-w-[100px] min-w-[100px] truncate">
<div className="bg-gray-900 dark:bg-gray-200 text-white bg-opacity-30 dark:bg-opacity-20 font-semibold backdrop-blur-2xl my-3 rounded-full px-1.5 py-0.5 max-w-[100px] truncate">
{currentFormattedDate}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import { EmojiPicker } from "../../../components/emoji-picker";
import { EmojiPicker } from "../../../../components/emoji-picker";
import {
attachFileSvg,
chatBoxImageSvg,
Expand All @@ -8,36 +8,32 @@ import {
imageSvg,
informationOutlineSvg,
messageSendSvg
} from "../../../img/svg";
import { GifImagesStyle } from "./chat-popup/chat-constants";
import { _t } from "../../../i18n";
} from "../../../../img/svg";
import { GifImagesStyle } from "../chat-popup/chat-constants";
import { _t } from "../../../../i18n";
import { Form } from "@ui/form";
import { FormControl } from "@ui/input";
import { Button } from "@ui/button";
import { Dropdown, DropdownItemWithIcon, DropdownMenu, DropdownToggle } from "@ui/dropdown";
import GifPicker from "../../../components/gif-picker";
import GifPicker from "../../../../components/gif-picker";
import useClickAway from "react-use/lib/useClickAway";
import { Spinner } from "@ui/spinner";
import {
Channel,
ChatContext,
DirectContact,
useGetPublicKeysQuery,
useSendMessage
} from "@ecency/ns-query";
import Tooltip from "../../../components/tooltip";
import { ChatInputFiles } from "./chat-input-files";
import Gallery from "../../../components/gallery";
import { Channel, ChatContext, DirectContact, useGetPublicKeysQuery } from "@ecency/ns-query";
import Tooltip from "../../../../components/tooltip";
import { ChatInputFiles } from "../chat-input-files";
import Gallery from "../../../../components/gallery";
import useWindowSize from "react-use/lib/useWindowSize";
import "./_chats.scss";
import { useMappedStore } from "../../../store/use-mapped-store";
import { useMappedStore } from "../../../../store/use-mapped-store";
import { ChatReplyDirectMessage } from "../reply-to-messages";
import { useChatInputSubmit } from "./hooks";

interface Props {
currentChannel?: Channel;
currentContact?: DirectContact;
}

export default function ChatInput({ currentChannel, currentContact }: Props) {
export function ChatInput({ currentChannel, currentContact }: Props) {
const size = useWindowSize();
const { global } = useMappedStore();

Expand All @@ -57,13 +53,6 @@ export default function ChatInput({ currentChannel, currentContact }: Props) {
const { data: contactKeys, isLoading: isContactKeysLoading } = useGetPublicKeysQuery(
currentContact?.name
);
const { mutateAsync: sendMessage, isLoading: isSendMessageLoading } = useSendMessage(
currentChannel,
currentContact,
() => {
setMessage("");
}
);

const isCurrentUser = useMemo(() => !!currentContact, [currentContact]);
const isCommunity = useMemo(() => !!currentChannel, [currentChannel]);
Expand All @@ -75,11 +64,11 @@ export default function ChatInput({ currentChannel, currentContact }: Props) {
const isJoined = useMemo(() => (contactKeys ? contactKeys.pubkey : false), [contactKeys]);
const isReadOnly = useMemo(
() => (contactKeys && isJoined ? currentContact?.pubkey !== contactKeys.pubkey : false),
[contactKeys, currentContact, isJoined]
[contactKeys, isJoined, currentContact?.pubkey]
);
const isFilesUploading = useMemo(
() => (files.length > 0 ? files.length !== uploadedFileLinks.length : false),
[files, uploadedFileLinks]
[files.length, uploadedFileLinks.length]
);

useClickAway(gifPickerRef, () => setShowGifPicker(false));
Expand All @@ -90,21 +79,18 @@ export default function ChatInput({ currentChannel, currentContact }: Props) {
}
}, [isCommunity, isCurrentUser]);

const submit = async () => {
const nextMessage = buildImages(message);
if (isDisabled || isSendMessageLoading || isFilesUploading || !nextMessage) {
return;
}
await sendMessage({ message: nextMessage });
setFiles([]);
setUploadedFileLinks([]);
// Re-focus to input because when DOM changes and input position changes then
// focus is lost
setTimeout(() => inputRef.current?.focus(), 1);
};

const buildImages = (message: string) =>
`${message}${uploadedFileLinks.map((link) => `\n![](${link})`)}`;
const { submit, sendMessage, isSendMessageLoading } = useChatInputSubmit({
currentChannel,
currentContact,
inputRef,
message,
setMessage,
uploadedFileLinks,
setUploadedFileLinks,
setFiles,
isDisabled,
isFilesUploading
});

return (
<div className="chat-input">
Expand All @@ -117,6 +103,7 @@ export default function ChatInput({ currentChannel, currentContact }: Props) {
</div>
) : (
<>
{currentContact && <ChatReplyDirectMessage currentContact={currentContact} />}
{showGifPicker && (
<GifPicker
rootRef={gifPickerRef}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./use-chat-input-submit";
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { MutableRefObject, useCallback } from "react";
import { usePersistentReplyToMessage } from "../../../hooks";
import { Channel, DirectContact, useSendMessage } from "@ecency/ns-query";

interface Props {
message: string;
isDisabled: boolean;
isFilesUploading: boolean;
currentChannel?: Channel;
currentContact?: DirectContact;
setMessage: (message: string) => void;
setFiles: (files: File[]) => void;
setUploadedFileLinks: (links: string[]) => void;
uploadedFileLinks: string[];
inputRef: MutableRefObject<HTMLInputElement | null>;
}

export function useChatInputSubmit({
message,
isFilesUploading,
isDisabled,
currentChannel,
currentContact,
setMessage,
setFiles,
setUploadedFileLinks,
uploadedFileLinks,
inputRef
}: Props) {
const [reply, _, clearReply] = usePersistentReplyToMessage(currentChannel, currentContact);

const { mutateAsync: sendMessage, isLoading: isSendMessageLoading } = useSendMessage(
currentChannel,
currentContact,
() => {
setMessage("");
}
);

const submit = useCallback(async () => {
const nextMessage = buildImages(message);

if (isDisabled || isSendMessageLoading || isFilesUploading || !nextMessage) {
return;
}

const request: Parameters<typeof sendMessage>[0] = { message: nextMessage };
if (reply) {
request.parentMessage = reply;
}

await sendMessage(request);

clearReply();
setFiles([]);
setUploadedFileLinks([]);
// Re-focus to input because when DOM changes and input position changes then
// focus is lost
setTimeout(() => inputRef.current?.focus(), 1);
}, [message, isDisabled, isSendMessageLoading, isFilesUploading, sendMessage, reply, clearReply]);

const buildImages = (message: string) =>
`${message}${uploadedFileLinks.map((link) => `\n![](${link})`)}`;

return { submit, sendMessage, isSendMessageLoading } as const;
}
2 changes: 2 additions & 0 deletions src/common/features/chats/components/chat-input/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./chat-input";
export * from "./hooks";
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { classNameObject } from "../../../../helper/class-name-object";
import { _t } from "../../../../i18n";
import ProfileLink from "../../../../components/profile-link";
import { history } from "../../../../store";
import React from "react";
import { Message, useNostrGetUserProfileQuery } from "@ecency/ns-query";
import { useMappedStore } from "../../../../store/use-mapped-store";

interface Props {
message: Message;
type: "receiver" | "sender";
}

export function ChatMessageForwardingLabel({ message, type }: Props) {
const { addAccount } = useMappedStore();

const { data: nostrForwardedUserProfiles } = useNostrGetUserProfileQuery(message.forwardedFrom);

return message.forwardedFrom ? (
<div
className={classNameObject({
"text-xs text-gray-300": true,
"dark:text-gray-700": type === "receiver"
})}
>
{_t("chat.forwarded-from")}
{nostrForwardedUserProfiles?.[0]?.name && (
<ProfileLink
target="_blank"
addAccount={addAccount}
history={history!!}
username={nostrForwardedUserProfiles?.[0]?.name}
>
<span
className={classNameObject({
"text-xs text-gray-300": true,
"dark:text-gray-700": type === "receiver"
})}
>
({nostrForwardedUserProfiles[0].name})
</span>
</ProfileLink>
)}
</div>
) : (
<></>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { classNameObject } from "../../../../helper/class-name-object";
import React from "react";
import { DirectContact, Message } from "@ecency/ns-query";
import { useFocusOnMessageById } from "./hooks";

interface Props {
message: Message;
currentContact?: DirectContact;
type: "receiver" | "sender";
}

export function ChatMessageRepliedLabel({ message, currentContact, type }: Props) {
const { focus } = useFocusOnMessageById(message.parentMessage?.id);

return message.parentMessage && currentContact ? (
<div
className={classNameObject({
"rounded-b-xl py-1 px-3 mb-1.5 truncate cursor-pointer": true,
"bg-blue-dark-sky-010 text-white rounded-tl-xl dark:blue-dark-sky-active":
type === "sender",
"bg-gray-300 dark:bg-gray-700 rounded-tr-xl": type === "receiver"
})}
onClick={() => focus()}
>
<div className="text-xs font-semibold">{currentContact?.name}</div>
<div className="text-xs">{message.parentMessage.content}</div>
</div>
) : (
<></>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { format } from "date-fns";
import { Spinner } from "@ui/spinner";
import { failedMessageSvg } from "../../../../img/svg";
import { Button } from "@ui/button";
import { _t } from "../../../../i18n";
import React from "react";
import { Channel, DirectContact, Message, useResendMessage } from "@ecency/ns-query";

interface Props {
message: Message;
showDate?: boolean;
currentChannel?: Channel;
currentContact?: DirectContact;
}

export function ChatMessageStatus({ message, showDate, currentContact, currentChannel }: Props) {
const { mutateAsync: resendMessage } = useResendMessage(currentChannel, currentContact);

return (
<>
{message.sent == 1 && showDate && (
<div className="text-gray-600 dark:text-gray-400 text-xs px-2">
{format(new Date(message.created * 1000), "HH:mm")}
</div>
)}
{message.sent === 0 && <Spinner className="w-3 h-3 mx-2" />}
{message.sent === 2 && (
<div className="flex items-center gap-1">
{failedMessageSvg}
<Button
size="xs"
className="text-xs"
appearance="link"
noPadding={true}
onClick={() => resendMessage(message)}
>
{_t("g.resend")}
</Button>
</div>
)}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Link } from "react-router-dom";
import { classNameObject } from "../../../../helper/class-name-object";
import { makePath } from "../../../../components/profile-link";
import React from "react";
import { Profile } from "@ecency/ns-query";

interface Props {
showUsername?: boolean;
isGif: boolean;
isEmoji: boolean;
profile?: Profile;
}

export function ChatMessageUsernameLabel({ showUsername, isEmoji, isGif, profile }: Props) {
return showUsername ? (
<Link
className={classNameObject({
"font-semibold text-sm mb-2 text-blue-dark-sky": true,
"px-2.5": isGif || isEmoji
})}
style={{
display: "inherit"
}}
to={makePath(profile!!.name)}
>
{profile!!.name}
</Link>
) : (
<></>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./use-focus-on-message-by-id";
Loading

0 comments on commit aaf957d

Please sign in to comment.