Skip to content
This repository has been archived by the owner on Jan 15, 2025. It is now read-only.

Commit

Permalink
Merge pull request #1530 from ecency/feature/chats-1.1
Browse files Browse the repository at this point in the history
Feature/chats 1.1
  • Loading branch information
feruzm authored Jan 11, 2024
2 parents 07f59c4 + 06d5a41 commit 90d9571
Show file tree
Hide file tree
Showing 18 changed files with 342 additions and 120 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"start:prod": "NODE_ENV=production node build/server.js"
},
"dependencies": {
"@ecency/ns-query": "^1.0.2",
"@ecency/ns-query": "^1.0.4",
"@ecency/render-helper": "^2.2.29",
"@ecency/render-helper-amp": "^1.1.0",
"@emoji-mart/data": "^1.1.2",
Expand Down
16 changes: 14 additions & 2 deletions src/common/app.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useMemo, useState } from "react";
import { Route, Switch } from "react-router-dom";
import EntryIndexContainer from "./pages/index";
import { EntryScreen } from "./pages/entry";
Expand Down Expand Up @@ -33,6 +33,8 @@ import { ChatPopUp } from "./features/chats/components/chat-popup";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ChatContextProvider } from "@ecency/ns-query";
import { useGetAccountFullQuery } from "./api/queries";
import defaults from "./constants/defaults.json";
import { getAccessToken } from "./helper/user-token";

// Define lazy pages
const ProfileContainer = loadable(() => import("./pages/profile-functional"));
Expand Down Expand Up @@ -105,13 +107,23 @@ const App = (props: any) => {
}
});

const accessToken = useMemo(
() => (activeUser ? getAccessToken(activeUser.username) ?? "" : ""),
[activeUser]
);

return (
<EntriesCacheManager>
{/*Excluded from production*/}
<ReactQueryDevtools initialIsOpen={false} />
<Tracker />
<UserActivityRecorder />
<ChatContextProvider activeUsername={activeUser?.username} activeUserData={activeUserAccount}>
<ChatContextProvider
privateApiHost={defaults.base}
activeUsername={activeUser?.username}
activeUserData={activeUserAccount}
ecencyAccessToken={accessToken}
>
<Switch>
<Route exact={true} path={routes.HOME} component={EntryIndexContainer} />
<Route exact={true} strict={true} path={routes.FILTER} component={EntryIndexContainer} />
Expand Down
2 changes: 1 addition & 1 deletion src/common/components/profile-card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export const ProfileCard = (props: Props) => {
))}
</div>
)}
<div className="btn-controls flex gap-3">
<div className="btn-controls flex flex-wrap gap-3">
{isCommunity(account?.name) && (
<>
<Link to={`/created/${account?.name}`}>
Expand Down
30 changes: 22 additions & 8 deletions src/common/features/chats/components/chat-channel-messages.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { useMappedStore } from "../../../store/use-mapped-store";
import { _t } from "../../../i18n";
import { ChatMessageItem } from "./chat-message-item";
Expand All @@ -17,6 +17,7 @@ import {
import { groupMessages } from "../utils";
import { ChatFloatingDate } from "./chat-floating-date";
import { differenceInCalendarDays } from "date-fns";
import useDebounce from "react-use/lib/useDebounce";

interface Props {
publicMessages: PublicMessage[];
Expand All @@ -31,9 +32,10 @@ export function ChatsChannelMessages({ publicMessages, currentChannel, isPage }:

// Message where users interacted with context menu
const [currentInteractingMessageId, setCurrentInteractingMessageId] = useState<string>();
const [needFetchNextPage, setNeedFetchNextPage] = useState(false);

const { publicKey } = useKeysQuery();
const { fetchNextPage } = usePublicMessagesQuery(currentChannel);
const { fetchNextPage, refetch } = usePublicMessagesQuery(currentChannel);

const { mutateAsync: updateBlockedUsers, isLoading: isUsersBlockingLoading } =
useUpdateChannelBlockedUsers(currentChannel);
Expand All @@ -49,6 +51,22 @@ export function ChatsChannelMessages({ publicMessages, currentChannel, isPage }:
);
const groupedMessages = useMemo(() => groupMessages(messages), [messages]);

useEffect(() => {
if (messages.length === 0) {
refetch();
}
}, [messages]);

useDebounce(
() => {
if (needFetchNextPage) {
fetchNextPage();
}
},
500,
[needFetchNextPage]
);

return (
<>
<div className="channel-messages" ref={channelMessagesRef}>
Expand Down Expand Up @@ -88,12 +106,8 @@ export function ChatsChannelMessages({ publicMessages, currentChannel, isPage }:
100
)
}
onInViewport={() =>
i === groupedMessages.length - 1 &&
j === group.length - 1 &&
fetchNextPage({
pageParam: message.created * 1000
})
onInViewport={(inViewport) =>
i === 0 && j === 0 && setNeedFetchNextPage(inViewport)
}
/>
<DropdownMenu
Expand Down
108 changes: 108 additions & 0 deletions src/common/features/chats/components/chat-input-files.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { Dispatch, SetStateAction, useMemo } from "react";
import { useChatFileUpload } from "../mutations";
import { CHAT_FILE_CONTENT_TYPES } from "./chat-popup/chat-constants";
import { Spinner } from "@ui/spinner";
import { classNameObject } from "../../../helper/class-name-object";
import { Button } from "@ui/button";
import { deleteForeverSvg } from "../../../img/svg";
import useMount from "react-use/lib/useMount";
import { _t } from "../../../i18n";

interface FileItemProps {
file?: File;
link?: string;
isUploading?: boolean;
onDelete: () => void;
onUpload?: (url: string) => void;
}

function FileItem({ link, file, onDelete, isUploading = false, onUpload }: FileItemProps) {
const {
mutateAsync: upload,
isLoading,
isError
} = useChatFileUpload((_, url) => onUpload?.(url));

useMount(() => {
if (file) {
upload(file);
}
});

return (
<div
className={classNameObject({
"w-[6rem] h-[6rem] bg-cover rounded-2xl flex relative items-center justify-center overflow-hidden":
true,
grayscale: isUploading
})}
style={{ backgroundImage: `url(${link})` }}
>
{!isLoading && (
<Button
appearance="gray-link"
icon={deleteForeverSvg}
size="xs"
className="absolute top-1 right-1 cursor-pointer"
onClick={() => onDelete()}
/>
)}
{isError && file && (
<Button size="xs" onClick={() => upload(file)}>
{_t("g.retry")}
</Button>
)}
{!isError && (isLoading || isUploading) && <Spinner className="w-4 h-4" />}
</div>
);
}

interface Props {
files: File[];
setFiles: Dispatch<SetStateAction<File[]>>;
uploadedFileLinks: string[];
setUploadedFileLinks: Dispatch<SetStateAction<string[]>>;
}

export function ChatInputFiles({
files,
setFiles,
uploadedFileLinks,
setUploadedFileLinks
}: Props) {
const fileList = useMemo(
() =>
[...files]
.filter((file) => {
const filenameLow = file.name.toLowerCase();
return CHAT_FILE_CONTENT_TYPES.some((el) => filenameLow.endsWith(el));
})
.map((file) => [file, URL.createObjectURL(file)] as const),
[files]
);

return (
<div className="bg-white bg-opacity-50 backdrop-blur border-t border-[--border-color] p-3 z-10 left-[-0.5rem] overflow-x-auto w-[calc(100%+0.5rem)] absolute bottom-[100%] flex gap-4">
{uploadedFileLinks.map((item) => (
<FileItem
key={item}
link={item}
onDelete={() => setUploadedFileLinks(uploadedFileLinks.filter((f) => f !== item))}
/>
))}
{fileList.map(([file, item]) => (
<FileItem
file={file}
key={item}
link={item}
isUploading={true}
onDelete={() => setFiles(files.filter((f) => f !== file))}
onUpload={(link) => {
setUploadedFileLinks((links) => [...links, link]);
setFiles((files) => files.filter((f) => f !== file));
}}
/>
))}
</div>
);
}
79 changes: 51 additions & 28 deletions src/common/features/chats/components/chat-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import {
chatBoxImageSvg,
emoticonHappyOutlineSvg,
gifIcon,
imageSvg,
informationOutlineSvg,
messageSendSvg
} from "../../../img/svg";
import { CHAT_FILE_CONTENT_TYPES, GifImagesStyle } from "./chat-popup/chat-constants";
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 { useChatFileUpload } from "../mutations";
import { Dropdown, DropdownItemWithIcon, DropdownMenu, DropdownToggle } from "@ui/dropdown";
import GifPicker from "../../../components/gif-picker";
import useClickAway from "react-use/lib/useClickAway";
Expand All @@ -28,6 +28,8 @@ import {
} from "@ecency/ns-query";
import { useGetAccountFullQuery } from "../../../api/queries";
import Tooltip from "../../../components/tooltip";
import { ChatInputFiles } from "./chat-input-files";
import Gallery from "../../../components/gallery";

interface Props {
currentChannel?: Channel;
Expand All @@ -45,10 +47,12 @@ export default function ChatInput({ currentChannel, currentContact }: Props) {
const { receiverPubKey } = useContext(ChatContext);

const [message, setMessage] = useState("");
const [files, setFiles] = useState<File[]>([]);
const [uploadedFileLinks, setUploadedFileLinks] = useState<string[]>([]);
const [showGifPicker, setShowGifPicker] = useState(false);
const [showGallery, setShowGallery] = useState(false);

const { data: contactData } = useGetAccountFullQuery(currentContact?.name);
const { mutateAsync: upload } = useChatFileUpload(setMessage);
const { mutateAsync: sendMessage, isLoading: isSendMessageLoading } = useSendMessage(
currentChannel,
currentContact,
Expand All @@ -68,6 +72,10 @@ export default function ChatInput({ currentChannel, currentContact }: Props) {
() => (contactData ? currentContact?.pubkey !== getUserChatPublicKey(contactData) : false),
[contactData, currentContact]
);
const isFilesUploading = useMemo(
() => (files.length > 0 ? files.length !== uploadedFileLinks.length : false),
[files, uploadedFileLinks]
);

useClickAway(gifPickerRef, () => setShowGifPicker(false));

Expand All @@ -77,25 +85,22 @@ export default function ChatInput({ currentChannel, currentContact }: Props) {
}
}, [isCommunity, isCurrentUser]);

const checkFile = (filename: string) => {
const filenameLow = filename.toLowerCase();
return CHAT_FILE_CONTENT_TYPES.some((el) => filenameLow.endsWith(el));
};

const fileInputChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
let files = [...(e.target.files as FileList)].filter((i) => checkFile(i.name)).filter((i) => i);

if (files.length > 0) {
e.stopPropagation();
e.preventDefault();
const submit = async () => {
if (isDisabled || isSendMessageLoading || isFilesUploading || !message) {
return;
}

files.forEach((file) => upload(file));

// reset input
e.target.value = "";
const nextMessage = buildImages(message);
await sendMessage(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})`)}`;

return (
<div className="chat-input">
{isReadOnly ? (
Expand All @@ -117,8 +122,25 @@ export default function ChatInput({ currentChannel, currentContact }: Props) {
fallback={(e) => sendMessage(e)}
/>
)}
{(files.length > 0 || uploadedFileLinks.length > 0) && !showGifPicker && (
<ChatInputFiles
files={files}
setFiles={setFiles}
uploadedFileLinks={uploadedFileLinks}
setUploadedFileLinks={setUploadedFileLinks}
/>
)}
{showGallery && (
<Gallery
onHide={() => setShowGallery(false)}
onPick={(e) => {
setUploadedFileLinks((links) => [...links, e]);
setShowGallery(false);
}}
/>
)}
<input
onChange={fileInputChanged}
onChange={(e) => setFiles([...(e.target.files ?? [])])}
className="hidden"
ref={fileInputRef}
type="file"
Expand All @@ -130,11 +152,7 @@ export default function ChatInput({ currentChannel, currentContact }: Props) {
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
sendMessage(message).then(() => {
// Re-focus to input because when DOM changes and input position changes then
// focus is lost
setTimeout(() => inputRef.current?.focus(), 1);
});
submit();
}}
className="w-full flex items-center gap-2 p-1.5"
>
Expand All @@ -156,9 +174,14 @@ export default function ChatInput({ currentChannel, currentContact }: Props) {
/>
<DropdownItemWithIcon
icon={chatBoxImageSvg}
label="Upload image"
label={_t("chat.upload-image")}
onClick={() => fileInputRef.current?.click()}
/>
<DropdownItemWithIcon
icon={imageSvg}
label={_t("user-nav.gallery")}
onClick={() => setShowGallery(true)}
/>
</DropdownMenu>
</Dropdown>
<FormControl
Expand Down Expand Up @@ -194,8 +217,8 @@ export default function ChatInput({ currentChannel, currentContact }: Props) {
noPadding={true}
appearance="gray-link"
icon={isSendMessageLoading ? <Spinner className="w-3.5 h-3.5" /> : messageSendSvg}
disabled={isDisabled || isSendMessageLoading}
onClick={() => sendMessage(message)}
disabled={isDisabled || isSendMessageLoading || isFilesUploading}
onClick={() => submit()}
/>
</div>
</Form>
Expand Down
Loading

0 comments on commit 90d9571

Please sign in to comment.