Browse Source

selected-messages-actions (#14)

BANK-1146: Масові дії з повідомленнями у веб-додатку
Reviewed-on: #14
Co-authored-by: Oksana Stepanenko <oksana.stepanenko@jetup.team>
Co-committed-by: Oksana Stepanenko <oksana.stepanenko@jetup.team>
pull/16/head
Oksana Stepanenko 8 months ago committed by Vitalik Yatsenko
parent
commit
f63084c1e0
  1. 23459
      package-lock.json
  2. 3
      package.json
  3. 3
      src/assets/img/3-dots.svg
  4. 10
      src/containers/Chats/atoms/audio-player.atom.tsx
  5. 1
      src/containers/Chats/atoms/index.ts
  6. 83
      src/containers/Chats/atoms/selected-messages-info.atom.tsx
  7. 39
      src/containers/Chats/atoms/style.scss
  8. 21
      src/containers/Chats/chats.screen.tsx
  9. 4
      src/containers/Chats/components/forward-message-modal.component.tsx
  10. 6
      src/containers/Chats/configs/chat-message-menu-options.config.ts
  11. 1
      src/containers/Chats/configs/index.ts
  12. 45
      src/containers/Chats/configs/selected-messages-menu-options.config.ts
  13. 6
      src/containers/Chats/consts/index.ts
  14. 1
      src/containers/Chats/enums/chat-message-action.enum.ts
  15. 4
      src/containers/Chats/enums/chat-view-mode.enum.ts
  16. 1
      src/containers/Chats/enums/index.ts
  17. 38
      src/containers/Chats/helpers/get-copied-messages-content.helper.ts
  18. 1
      src/containers/Chats/helpers/index.ts
  19. 2
      src/containers/Chats/hooks/index.ts
  20. 70
      src/containers/Chats/hooks/use-chat-messages.hook.ts
  21. 14
      src/containers/Chats/hooks/use-chat-view-mode-state.hook.ts
  22. 165
      src/containers/Chats/hooks/use-selected-messages.hook.tsx
  23. 10
      src/containers/Chats/modals/chat-confirm-delete-modal.tsx
  24. 14
      src/containers/Chats/plugins/chat-header.component.tsx
  25. 8
      src/containers/Chats/plugins/chat-item-file.component.tsx
  26. 7
      src/containers/Chats/plugins/chat-item-image.component.tsx
  27. 60
      src/containers/Chats/plugins/chat-item-video.component.tsx
  28. 57
      src/containers/Chats/plugins/chat-item.component.tsx
  29. 1
      src/containers/Chats/plugins/interfaces.ts
  30. 62
      src/containers/Chats/plugins/style.scss
  31. 17
      src/containers/Chats/smart-components/chats-select-list.smart-component.tsx
  32. 3
      src/containers/Chats/transforms/chat-messages.transforms.ts
  33. 22
      src/services/domain/chat-messages.service.ts
  34. 9
      src/shared/components/form/CheckBox.tsx
  35. 15
      src/shared/events/index.ts
  36. 1
      src/shared/helpers/index.ts
  37. 17
      src/shared/helpers/title-by-count.helper.ts
  38. 1
      src/shared/interfaces/messages.interfaces.ts

23459
package-lock.json generated

File diff suppressed because it is too large Load Diff

3
package.json

@ -172,7 +172,8 @@ @@ -172,7 +172,8 @@
"webpack-dev-server": "3.11.1",
"webpack-manifest-plugin": "2.2.0",
"workbox-webpack-plugin": "5.1.4",
"xlsx": "^0.15.5"
"xlsx": "^0.15.5",
"zustand": "^4.5.2"
},
"scripts": {
"start": "PORT=3001 node scripts/start.js",

3
src/assets/img/3-dots.svg

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
<svg width="4" height="14" viewBox="0 0 4 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 5C1.60444 5 1.21776 5.1173 0.888861 5.33706C0.559963 5.55683 0.303617 5.86918 0.152242 6.23463C0.000866562 6.60009 -0.0387401 7.00222 0.0384303 7.39018C0.115601 7.77814 0.306082 8.13451 0.585787 8.41422C0.865493 8.69392 1.22186 8.8844 1.60982 8.96157C1.99778 9.03874 2.39992 8.99914 2.76537 8.84776C3.13082 8.69639 3.44318 8.44004 3.66294 8.11114C3.8827 7.78224 4 7.39556 4 7C4 6.46957 3.78929 5.96086 3.41421 5.58579C3.03914 5.21072 2.53043 5 2 5ZM2 8C1.80222 8 1.60888 7.94135 1.44443 7.83147C1.27998 7.72159 1.15181 7.56541 1.07612 7.38269C1.00043 7.19996 0.980631 6.99889 1.01922 6.80491C1.0578 6.61093 1.15304 6.43275 1.29289 6.2929C1.43275 6.15304 1.61093 6.0578 1.80491 6.01922C1.99889 5.98063 2.19996 6.00043 2.38269 6.07612C2.56541 6.15181 2.72159 6.27998 2.83147 6.44443C2.94135 6.60888 3 6.80222 3 7C3 7.26522 2.89464 7.51957 2.70711 7.70711C2.51957 7.89464 2.26522 8 2 8ZM2 4C2.39556 4 2.78224 3.8827 3.11114 3.66294C3.44004 3.44318 3.69639 3.13082 3.84776 2.76537C3.99914 2.39992 4.03874 1.99778 3.96157 1.60982C3.8844 1.22186 3.69392 0.865492 3.41421 0.585787C3.13451 0.306082 2.77814 0.115601 2.39018 0.0384303C2.00222 -0.0387401 1.60009 0.000866562 1.23463 0.152242C0.869182 0.303617 0.556825 0.559962 0.337062 0.88886C0.117299 1.21776 1.07779e-06 1.60444 1.07779e-06 2C1.07779e-06 2.53043 0.210715 3.03914 0.585787 3.41422C0.96086 3.78929 1.46957 4 2 4ZM2 1C2.19778 1 2.39112 1.05865 2.55557 1.16853C2.72002 1.27841 2.84819 1.43459 2.92388 1.61732C2.99957 1.80004 3.01937 2.00111 2.98079 2.19509C2.9422 2.38907 2.84696 2.56726 2.70711 2.70711C2.56726 2.84696 2.38907 2.9422 2.19509 2.98079C2.00111 3.01937 1.80004 2.99957 1.61732 2.92388C1.43459 2.84819 1.27841 2.72002 1.16853 2.55557C1.05865 2.39112 1 2.19778 1 2C1 1.73478 1.10536 1.48043 1.29289 1.29289C1.48043 1.10536 1.73478 1 2 1ZM2 10C1.60444 10 1.21776 10.1173 0.888861 10.3371C0.559963 10.5568 0.303617 10.8692 0.152242 11.2346C0.000866562 11.6001 -0.0387401 12.0022 0.0384303 12.3902C0.115601 12.7781 0.306082 13.1345 0.585787 13.4142C0.865493 13.6939 1.22186 13.8844 1.60982 13.9616C1.99778 14.0387 2.39992 13.9991 2.76537 13.8478C3.13082 13.6964 3.44318 13.44 3.66294 13.1111C3.8827 12.7822 4 12.3956 4 12C4 11.4696 3.78929 10.9609 3.41421 10.5858C3.03914 10.2107 2.53043 10 2 10ZM2 13C1.80222 13 1.60888 12.9414 1.44443 12.8315C1.27998 12.7216 1.15181 12.5654 1.07612 12.3827C1.00043 12.2 0.980631 11.9989 1.01922 11.8049C1.0578 11.6109 1.15304 11.4327 1.29289 11.2929C1.43275 11.153 1.61093 11.0578 1.80491 11.0192C1.99889 10.9806 2.19996 11.0004 2.38269 11.0761C2.56541 11.1518 2.72159 11.28 2.83147 11.4444C2.94135 11.6089 3 11.8022 3 12C3 12.2652 2.89464 12.5196 2.70711 12.7071C2.51957 12.8946 2.26522 13 2 13Z" fill="#9E2743"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

10
src/containers/Chats/atoms/audio-player.atom.tsx

@ -7,6 +7,8 @@ import pauseIcon from "@/assets/img/pause-icon.svg"; @@ -7,6 +7,8 @@ import pauseIcon from "@/assets/img/pause-icon.svg";
import { AudioBar } from "./audio-bar.atom";
import "./style.scss";
import _ from "lodash";
import { useChatViewModeState } from "../hooks";
import { ChatViewModeEnum } from "../enums";
export const PlayBar = () => {
const { position, duration } = useAudioPosition({ highRefreshRate: true });
@ -36,10 +38,16 @@ export const AudioPlayer: FC<IAudioPlayerProps> = ({ @@ -36,10 +38,16 @@ export const AudioPlayer: FC<IAudioPlayerProps> = ({
activeAudioId,
onPressPlay,
}) => {
const viewMode = useChatViewModeState((s) => s.mode);
useEffect(() => {
if (activeAudioId !== fileId && playing) player.pause();
}, [activeAudioId]);
useEffect(() => {
if (viewMode === ChatViewModeEnum.SELECT && playing) player.pause();
}, [viewMode]);
const { togglePlayPause, ready, loading, playing, player } = useAudioPlayer({
src: fileUrl,
format: "mp4",
@ -52,6 +60,8 @@ export const AudioPlayer: FC<IAudioPlayerProps> = ({ @@ -52,6 +60,8 @@ export const AudioPlayer: FC<IAudioPlayerProps> = ({
if (loading) return <div>Loading audio</div>;
const actionHandler = async () => {
if (viewMode === ChatViewModeEnum.SELECT) return;
if (onPressPlay) onPressPlay();
await togglePlayPause();
};

1
src/containers/Chats/atoms/index.ts

@ -22,3 +22,4 @@ export * from "./set-user-admin-button.atom"; @@ -22,3 +22,4 @@ export * from "./set-user-admin-button.atom";
export * from "./forward-message-button.atom";
export * from "./pinned-limit-chat-modal.atom";
export * from "./pinned-message-button.atom";
export * from "./selected-messages-info.atom";

83
src/containers/Chats/atoms/selected-messages-info.atom.tsx

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
import React, { useMemo, useState } from "react";
import { ChatViewModeEnum } from "../enums";
import { useChatViewModeState } from "../hooks";
import DotsIcon from "@/assets/img/3-dots.svg";
import { Dropdown, Menu } from "antd";
import { IconComponent, getTitleByCount, useEventsListener } from "@/shared";
import { useChatSelectedMessagesState } from "../hooks";
import _ from "lodash";
import { ItemType } from "antd/lib/menu/hooks/useItems";
interface IProps {
onPressActionsBtn: () => void;
}
export const SelectedMessagesInfo = ({ onPressActionsBtn }: IProps) => {
const mode = useChatViewModeState((s) => s.mode);
const { setMode } = useChatViewModeState();
const selectedMessages = useChatSelectedMessagesState((s) => s.messages);
const { unselectAll: unselectAllMessages } = useChatSelectedMessagesState();
const [menuItems, setMenuItems] = useState([]);
const menu = useMemo(() => {
const preparedItems = menuItems.map((item) => ({
..._.pick(item, ["key", "label", "onClick"]),
icon: item.iconNode ? item.iconNode : null,
}));
return (
<Menu
className="context-menu-chat-item"
items={preparedItems as ItemType[]}
/>
);
}, [menuItems]);
useEventsListener(
"openSelectedMessagesMenuOptions",
(payload) => {
setMenuItems(payload.items);
},
[setMenuItems]
);
const infoText = useMemo(() => {
if (selectedMessages.length >= 1)
return `Вибрано ${getTitleByCount(selectedMessages.length, [
"повідомлення",
"повідомлення",
"повідомлень",
])}`;
return "Виберіть повідомлення";
}, [selectedMessages]);
if (mode === ChatViewModeEnum.DEFAULT) return <></>;
const cancelSelectMode = () => {
unselectAllMessages();
setMode(ChatViewModeEnum.DEFAULT);
};
return (
<div className="chat-selected-messages-info">
<div className="selected-messages-cancel-btn" onClick={cancelSelectMode}>
Скасувати
</div>
<div className="selected-messages-count-text">{infoText}</div>
{selectedMessages?.length > 0 && (
<Dropdown
onVisibleChange={onPressActionsBtn}
overlay={menu}
trigger={["click"]}
>
<div className="selected-messages-action-btn">
Дії
<IconComponent name={DotsIcon} />
</div>
</Dropdown>
)}
</div>
);
};

39
src/containers/Chats/atoms/style.scss

@ -297,3 +297,42 @@ $bg-color-lighter: rgba(158, 39, 67, 0.1); @@ -297,3 +297,42 @@ $bg-color-lighter: rgba(158, 39, 67, 0.1);
.error-tooltip {
color: red;
}
.chat-selected-messages-info {
display: flex;
flex-direction: row;
align-items: center;
gap: 34px;
font-size: 14px;
line-height: 16px;
}
.selected-messages-cancel-btn {
color: #9e2743;
&:hover {
cursor: pointer;
}
}
.selected-messages-count-text {
font-weight: 500;
}
.selected-messages-action-btn {
display: flex;
justify-content: center;
align-items: center;
width: 36px;
border-radius: 50%;
color: #9e2743;
img {
height: 15px;
margin-bottom: 2px;
}
&:hover {
cursor: pointer;
}
}

21
src/containers/Chats/chats.screen.tsx

@ -26,6 +26,9 @@ import { @@ -26,6 +26,9 @@ import {
useChatDetails,
useChatList,
useChatMessages,
useChatSelectedMessages,
useChatSelectedMessagesState,
useChatViewModeState,
useCreateTextMessage,
usePinedMessages,
useSendFiles,
@ -39,18 +42,34 @@ import _ from "lodash"; @@ -39,18 +42,34 @@ import _ from "lodash";
import { ChatConfirmDeleteModal } from "./modals/chat-confirm-delete-modal";
import { SelectStickersModalSmart } from "@/smart-components";
import { ChatSendImgModal } from "./components/chat-send-img-modal.component";
import { ChatViewModeEnum } from "./enums";
import { message } from "antd";
const ChatsPage: FC = () => {
const accountId = useSelector(getProfile);
const currentBgImgId = useSelector(selectCurrentChatBgId);
const selectedChatId = useSelector(selectSelectedChatId);
const [messageApi, contextHolder] = message.useMessage();
const [isCreateChatModal, setCreateChatModal] = useState<boolean>(false);
const [isMenuOpen, setMenuOpen] = useState<boolean>(false);
const [isOpenUserInfoModal, setUserInfoModal] = useState<boolean>(false);
const [selectedUserId, setSelectedUserId] = useState<number>();
const [chatBg, setBg] = useState<any>();
const { setMode } = useChatViewModeState();
const { unselectAll: unselectAllMessages } = useChatSelectedMessagesState();
const { openSelectedMessagesMenu } = useChatSelectedMessages({
infoMessageApi: messageApi,
});
useEffect(() => {
setMode(ChatViewModeEnum.DEFAULT);
unselectAllMessages();
}, [selectedChatId]);
const preparedBg = async () => {
if (currentBgImgId === ChatBgKeys.DEFAULT) return setBg(null);
@ -206,6 +225,7 @@ const ChatsPage: FC = () => { @@ -206,6 +225,7 @@ const ChatsPage: FC = () => {
return (
<Container>
{contextHolder}
<div className="chats">
<Row>
<CreateChatModal
@ -255,6 +275,7 @@ const ChatsPage: FC = () => { @@ -255,6 +275,7 @@ const ChatsPage: FC = () => {
setUnread={() =>
onSetUnread(selectedChatId, headerChatInfo)
}
onPressActionsBtn={() => openSelectedMessagesMenu(role)}
/>
<div

4
src/containers/Chats/components/forward-message-modal.component.tsx

@ -1,12 +1,12 @@ @@ -1,12 +1,12 @@
import React, { useState } from "react";
import { IMessage, useEventsListener } from "@/shared";
import ModalComponent from "@/components/Modal";
import { ChatsSelectListSmart } from "../smart-components";
import { IChatMessage } from "@/containers/Chats/plugins/interfaces";
export const ForwardMessageModal = () => {
const [isOpen, setIsOpen] = useState(false);
const [item, setItem] = useState<IMessage>(null);
const [item, setItem] = useState<IMessage | IChatMessage[]>(null);
useEventsListener(
"openForwardMessageModal",

6
src/containers/Chats/configs/chat-message-menu-options.config.ts

@ -65,6 +65,11 @@ export const getChatMessageMenuOptions = ({ @@ -65,6 +65,11 @@ export const getChatMessageMenuOptions = ({
label: "Видалити для всіх",
onClick: () => onClick(ChatMessageActionEnum.DELETE_FOR_ALL),
},
select: {
key: "select",
label: "Вибрати",
onClick: () => onClick(ChatMessageActionEnum.SELECT),
},
};
const optionsKeys = ["forward", "reply"];
@ -76,6 +81,7 @@ export const getChatMessageMenuOptions = ({ @@ -76,6 +81,7 @@ export const getChatMessageMenuOptions = ({
optionsKeys.push("delete");
if (canDeleteForAll) optionsKeys.push("deleteForAll");
if (canEdit) optionsKeys.push("edit");
optionsKeys.push("select");
const options = optionsKeys.map((key) => menuOptions[key]);

1
src/containers/Chats/configs/index.ts

@ -3,3 +3,4 @@ export * from "./chat-info-mock.config"; @@ -3,3 +3,4 @@ export * from "./chat-info-mock.config";
export * from "./chat-users-mock.config";
export * from "./attachments-menu.config";
export * from "./chat-message-menu-options.config";
export * from "./selected-messages-menu-options.config";

45
src/containers/Chats/configs/selected-messages-menu-options.config.ts

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
import { ChatMessageActionEnum } from "../enums";
interface IProps {
onClick: (actionType: ChatMessageActionEnum) => void;
canCopy?: boolean;
canDeleteForAll?: boolean;
}
export const getSelectedMessagesMenuOptions = ({
onClick,
canCopy,
canDeleteForAll,
}: IProps) => {
const menuOptions = {
forward: {
key: "forward",
label: "Переслати",
onClick: () => onClick(ChatMessageActionEnum.FORWARD),
},
copy: {
key: "copy",
label: "Копіювати",
onClick: () => onClick(ChatMessageActionEnum.COPY),
},
delete: {
key: "delete",
label: "Видалити",
onClick: () => onClick(ChatMessageActionEnum.DELETE),
},
deleteForAll: {
key: "deleteForAll",
label: "Видалити для всіх",
onClick: () => onClick(ChatMessageActionEnum.DELETE_FOR_ALL),
},
};
const optionsKeys = ["forward"];
if (canCopy) optionsKeys.push("copy");
optionsKeys.push("delete");
if (canDeleteForAll) optionsKeys.push("deleteForAll");
const options = optionsKeys.map((key) => menuOptions[key]);
return options;
};

6
src/containers/Chats/consts/index.ts

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
import { MessageType } from "@/shared";
export const COPY_ENABLED_MESSAGES_TYPES = [
MessageType.Text,
MessageType.Image,
];

1
src/containers/Chats/enums/chat-message-action.enum.ts

@ -8,4 +8,5 @@ export enum ChatMessageActionEnum { @@ -8,4 +8,5 @@ export enum ChatMessageActionEnum {
DELETE_FOR_ALL,
DOWNLOAD,
EDIT,
SELECT,
}

4
src/containers/Chats/enums/chat-view-mode.enum.ts

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
export enum ChatViewModeEnum {
DEFAULT,
SELECT,
}

1
src/containers/Chats/enums/index.ts

@ -1 +1,2 @@ @@ -1 +1,2 @@
export * from "./chat-message-action.enum";
export * from "./chat-view-mode.enum";

38
src/containers/Chats/helpers/get-copied-messages-content.helper.ts

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
import { MessageType } from "@/shared";
import _ from "lodash";
import moment from "moment";
import { IChatMessage } from "../plugins/interfaces";
export const getCopiedContent = (items: Partial<IChatMessage>[]) => {
if (items.length === 1 && items[0].type === MessageType.Image) {
return items[0].content.fileUrl;
} else if (_.every(items, (it) => it.type === MessageType.Text)) {
return createCopiedMessagesContent(items);
}
};
function createCopiedMessagesContent(items: Partial<IChatMessage>[]) {
const text = [];
const sortedItems = _.orderBy(items, ["createdAt"], ["asc"]);
for (const item of sortedItems) {
const formattedContent = getFormattedMessageContent(item);
text.push(formattedContent);
}
return text.join("\n");
}
function getFormattedMessageContent(item: Partial<IChatMessage>) {
const dateAndTime = getDateAndTime(item);
const authorName = _.defaultTo(item.author?.name, "");
const text = _.defaultTo(item.content?.message, "");
return `[${dateAndTime}] ${authorName}: ${text}`;
}
function getDateAndTime(item: Partial<IChatMessage>) {
if (!item.createdAt) return "";
return moment(item.createdAt).format("DD.MM.YY, HH:mm");
}

1
src/containers/Chats/helpers/index.ts

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
export * from "./get-chat-info.helper";
export * from "./get-header-chat-info.helper";
export * from "./chat-messages.helper";
export * from "./get-copied-messages-content.helper";

2
src/containers/Chats/hooks/index.ts

@ -11,3 +11,5 @@ export * from "./use-selected-chats.hook"; @@ -11,3 +11,5 @@ export * from "./use-selected-chats.hook";
export * from "./use-send-files.hook";
export * from "./use-pined-messages.hook";
export * from "./use-chats-settings.hook";
export * from "./use-chat-view-mode-state.hook";
export * from "./use-selected-messages.hook";

70
src/containers/Chats/hooks/use-chat-messages.hook.ts

@ -2,13 +2,13 @@ import { @@ -2,13 +2,13 @@ import {
ChatMemberRole,
ChatMessageEventType,
IChatMessage,
MessageType
MessageType,
} from "@/shared";
import {
useEventsListener,
useIdsList,
useSocket,
useSocketListener
useSocketListener,
} from "@/shared/hooks/";
import _ from "lodash";
import { useCallback, useEffect, useState } from "react";
@ -17,9 +17,10 @@ import { IFetchChatMessages } from "@/api/chats-messages/request.interfaces"; @@ -17,9 +17,10 @@ import { IFetchChatMessages } from "@/api/chats-messages/request.interfaces";
import { useSelector } from "react-redux";
import { getProfile } from "@/store/account";
import { appEvents } from "@/shared/events";
import { ChatMessageActionEnum } from "../enums";
import { ChatMessageActionEnum, ChatViewModeEnum } from "../enums";
import { getChatMessageMenuOptions } from "../configs";
import { chatMessagesApi } from "@/api";
import { useChatViewModeState } from "./use-chat-view-mode-state.hook";
export const useChatMessages = (
chatId: number,
@ -32,6 +33,8 @@ export const useChatMessages = ( @@ -32,6 +33,8 @@ export const useChatMessages = (
const [replyTo, setReplyTo] = useState<IChatMessage>(null);
const [scrollToId, setScrollToId] = useState(null);
const { setMode: setChatViewMode } = useChatViewModeState();
const {
items: messages,
loadNew,
@ -44,14 +47,14 @@ export const useChatMessages = ( @@ -44,14 +47,14 @@ export const useChatMessages = (
resetList,
_setItems,
loadParams,
setLoadParams
setLoadParams,
} = useIdsList<IChatMessage, IFetchChatMessages>({
limit: 20,
req: async params => await chatMessagesService.fetchMessages(params),
req: async (params) => await chatMessagesService.fetchMessages(params),
needInit: false,
clearWhenReload: false,
lastMessageId,
firstMessageId
firstMessageId,
});
// const afterAction = () => {
@ -76,7 +79,7 @@ export const useChatMessages = ( @@ -76,7 +79,7 @@ export const useChatMessages = (
appEvents.emit("openConfirmDeleteMessageModal", {
message,
deleteForAll,
isShow: true
isShow: true,
}),
300
);
@ -93,11 +96,11 @@ export const useChatMessages = ( @@ -93,11 +96,11 @@ export const useChatMessages = (
const isAuthor = message.userId === account.id;
if (_.isEmpty(message.events) && isAuthor) return true;
const viewed = _.some(message.events, event => {
const viewed = _.some(message.events, (event) => {
const keys = Object.keys(event);
return _.some(
keys,
key => Number(key) !== account.id && event[key] === "view"
(key) => Number(key) !== account.id && event[key] === "view"
);
});
@ -126,6 +129,8 @@ export const useChatMessages = ( @@ -126,6 +129,8 @@ export const useChatMessages = (
appEvents.emit("onPressEditmessage", { message });
};
const onSelect = () => setChatViewMode(ChatViewModeEnum.SELECT);
const actions = {
[ChatMessageActionEnum.FORWARD]: onForward,
[ChatMessageActionEnum.DELETE]: onDelete,
@ -134,7 +139,8 @@ export const useChatMessages = ( @@ -134,7 +139,8 @@ export const useChatMessages = (
[ChatMessageActionEnum.COPY]: onCopy,
[ChatMessageActionEnum.PIN]: onPin,
[ChatMessageActionEnum.UNPIN]: onUnpin,
[ChatMessageActionEnum.EDIT]: onEdit
[ChatMessageActionEnum.EDIT]: onEdit,
[ChatMessageActionEnum.SELECT]: onSelect,
};
const onMessageActions = (message: IChatMessage, role: ChatMemberRole) => {
@ -153,7 +159,7 @@ export const useChatMessages = ( @@ -153,7 +159,7 @@ export const useChatMessages = (
canCopy,
onClick: (actionType: ChatMessageActionEnum) =>
actions[actionType](message),
canEdit
canEdit,
});
appEvents.emit("openMessageMenuOptions", { items: options });
@ -179,24 +185,24 @@ export const useChatMessages = ( @@ -179,24 +185,24 @@ export const useChatMessages = (
}, [chatId]);
const messageContainsItem = (itemId: number) =>
_.find(messages, message => message.id === itemId);
_.find(messages, (message) => message.id === itemId);
const updateEvents = (
messages: IChatMessage[],
userId: number,
event: ChatMessageEventType
) => {
const updatedItems = messages.map(item => {
const updatedItems = messages.map((item) => {
const userEvent = _.find(
item.events,
it =>
(it) =>
_.includes(Object.keys(it), userId.toString()) && it[userId] === event
);
if (userEvent) return item;
return {
...item,
events: [...item.events, { userId: event }]
events: [...item.events, { userId: event }],
};
});
@ -213,7 +219,7 @@ export const useChatMessages = ( @@ -213,7 +219,7 @@ export const useChatMessages = (
socket.emit("chat/read-message", {
userId: account.id,
messagesIds: [message.id],
chatId
chatId,
});
if (message.userId === account.id) setScrollToId(message.id);
@ -226,18 +232,18 @@ export const useChatMessages = ( @@ -226,18 +232,18 @@ export const useChatMessages = (
{
const messagesToAdd = _.filter(
message,
it => !messageContainsItem(it.id)
(it) => !messageContainsItem(it.id)
);
if (!_.isEmpty(messagesToAdd)) {
_setItems([...messagesToAdd, ...messages]);
const messagesIds = messagesToAdd.map(it => it.id);
const messagesIds = messagesToAdd.map((it) => it.id);
socket.emit("chat/read-message", {
userId: account.id,
messagesIds,
chatId
chatId,
});
}
}
@ -263,15 +269,15 @@ export const useChatMessages = ( @@ -263,15 +269,15 @@ export const useChatMessages = (
const onPinedMessage = (data: { chatId: number; messageId: number }) => {
if (
data?.chatId !== chatId ||
!_.find(messages, message => message.id === data.messageId)
!_.find(messages, (message) => message.id === data.messageId)
)
return;
const newItems = messages.map(item => {
const newItems = messages.map((item) => {
if (item.id === data.messageId)
return {
...item,
isPined: !item.isPined
isPined: !item.isPined,
};
return item;
});
@ -282,7 +288,7 @@ export const useChatMessages = ( @@ -282,7 +288,7 @@ export const useChatMessages = (
const onMessageDeleted = (data: { messageId: number; chatId: number }) => {
if (data?.chatId !== chatId) return;
const updatedMessages = messages.map(item => {
const updatedMessages = messages.map((item) => {
if (
item.content?.replyToMessage &&
item.content?.replyToMessage?.id === data.messageId
@ -293,16 +299,16 @@ export const useChatMessages = ( @@ -293,16 +299,16 @@ export const useChatMessages = (
...item.content,
replyToMessage: {
...item.content.replyToMessage,
isDeleted: true
}
}
isDeleted: true,
},
},
};
return item;
});
const filteredItems = _.filter(
updatedMessages,
item => item.id !== data?.messageId
(item) => item.id !== data?.messageId
);
_setItems(filteredItems);
@ -315,7 +321,7 @@ export const useChatMessages = ( @@ -315,7 +321,7 @@ export const useChatMessages = (
const onUpdateMessage = ({ message }) => {
console.log("on update message");
const newItems = messages.map(it => {
const newItems = messages.map((it) => {
if (it.id === message.id) {
return message;
}
@ -357,7 +363,7 @@ export const useChatMessages = ( @@ -357,7 +363,7 @@ export const useChatMessages = (
useSocketListener(
"chat/new-message",
data => {
(data) => {
onNewMessage(data);
},
[chatId, messages]
@ -365,7 +371,7 @@ export const useChatMessages = ( @@ -365,7 +371,7 @@ export const useChatMessages = (
useSocketListener("chat/delete-message", onMessageDeleted, [
chatId,
messages
messages,
]);
useSocketListener("chat/pined-message", onPinedMessage, [chatId, messages]);
@ -384,7 +390,7 @@ export const useChatMessages = ( @@ -384,7 +390,7 @@ export const useChatMessages = (
isLoadingNew,
isLoadingOld,
lastMessageLoaded,
firstMessageLoaded
firstMessageLoaded,
},
onMessageActions,
@ -394,6 +400,6 @@ export const useChatMessages = ( @@ -394,6 +400,6 @@ export const useChatMessages = (
onForward,
onPressMessagePreview,
replyTo,
setReplyToMessage: setReplyTo
setReplyToMessage: setReplyTo,
};
};

14
src/containers/Chats/hooks/use-chat-view-mode-state.hook.ts

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
import { create } from 'zustand'
import { ChatViewModeEnum } from '../enums'
interface IChatViewModeState {
mode: ChatViewModeEnum
setMode: (mode: ChatViewModeEnum) => void
}
export const useChatViewModeState = create<IChatViewModeState>()(set => ({
mode: ChatViewModeEnum.DEFAULT,
setMode: (mode: ChatViewModeEnum) => {
set({ mode })
},
}))

165
src/containers/Chats/hooks/use-selected-messages.hook.tsx

@ -0,0 +1,165 @@ @@ -0,0 +1,165 @@
import _ from "lodash";
import { IChatMessage } from "../plugins/interfaces";
import { create } from "zustand";
import { ChatMemberRole, MessageType } from "@/shared";
import { ChatMessageActionEnum, ChatViewModeEnum } from "../enums";
import { useChatViewModeState } from "./use-chat-view-mode-state.hook";
import { appEvents } from "@/shared/events";
import { getCopiedContent } from "../helpers";
import { COPY_ENABLED_MESSAGES_TYPES } from "../consts";
import { getSelectedMessagesMenuOptions } from "../configs";
import React from "react";
import { MessageInstance } from "antd/lib/message";
interface IChatSelectedMessagesState {
messages: Partial<IChatMessage>[];
selectMessage: (message: Partial<IChatMessage>) => void;
unselectAll: () => void;
}
export const useChatSelectedMessagesState = create<
IChatSelectedMessagesState
>()((set) => ({
messages: [],
selectMessage(message: Partial<IChatMessage>) {
set((state) => {
if (_.find(state.messages, (it) => it.id === message.id))
return {
messages: state.messages.filter((it) => it.id !== message.id),
};
else
return {
messages: [
...state.messages,
_.pick(message, [
"id",
"type",
"content",
"authorId",
"chatId",
"read",
"isMy",
"author",
"isPined",
"createdAt",
]),
],
};
});
},
unselectAll() {
set({ messages: [] });
},
}));
interface IProps {
infoMessageApi: MessageInstance;
}
export const useChatSelectedMessages = ({ infoMessageApi }: IProps) => {
const messages = useChatSelectedMessagesState((s) => s.messages);
const { unselectAll } = useChatSelectedMessagesState();
const { setMode } = useChatViewModeState();
const actions = {
[ChatMessageActionEnum.FORWARD]: forwardMany,
[ChatMessageActionEnum.COPY]: copyMany,
[ChatMessageActionEnum.DELETE]: deleteMany,
[ChatMessageActionEnum.DELETE_FOR_ALL]: deleteForAllMany,
};
const afterAction = () => {
unselectAll();
setMode(ChatViewModeEnum.DEFAULT);
};
function forwardMany() {
appEvents.emit("openForwardMessageModal", {
message: messages as IChatMessage[],
isShow: true,
});
afterAction();
}
function copyMany() {
const copiedContent = getCopiedContent(messages);
navigator.clipboard.writeText(copiedContent);
infoMessageApi.open({
type: "success",
icon: <></>,
className: "custom-info-message",
content: "Повідомлення скопійовано",
style: {
marginTop: 60,
},
});
afterAction();
}
function deleteMany() {
deleteMessages();
}
function deleteForAllMany() {
deleteMessages(true);
}
function deleteMessages(deleteForAll?: boolean) {
setTimeout(
() =>
appEvents.emit("openConfirmDeleteMessageModal", {
message: messages,
deleteForAll,
isShow: true,
onSuccess: afterAction,
}),
300
);
}
const openSelectedMessagesMenu = (userRoleInChat: ChatMemberRole) => {
const canCopy = checkCanCopy();
const canDeleteForAll = checkCanDeleteForAll();
function checkCanCopy() {
if (
messages.length === 1 &&
COPY_ENABLED_MESSAGES_TYPES.includes(messages[0].type)
)
return true;
if (
messages.length > 1 &&
_.every(messages, (it) => it.type === MessageType.Text)
)
return true;
return false;
}
function checkCanDeleteForAll() {
if (userRoleInChat === ChatMemberRole.Admin) return true;
return _.every(messages, (it) => {
if (!it.read && it.isMy) return true;
return false;
});
}
const options = getSelectedMessagesMenuOptions({
canDeleteForAll,
canCopy,
onClick: (actionType: ChatMessageActionEnum) =>
actions[actionType](messages),
});
appEvents.emit("openSelectedMessagesMenuOptions", { items: options });
};
return {
openSelectedMessagesMenu,
};
};

10
src/containers/Chats/modals/chat-confirm-delete-modal.tsx

@ -2,12 +2,17 @@ import { chatMessagesService } from "@/services/domain"; @@ -2,12 +2,17 @@ import { chatMessagesService } from "@/services/domain";
import { IChatMessage, useEventsListener } from "@/shared";
import React, { useState } from "react";
import Modal from "../../../components/Modal";
import { IChatMessage as IChatMessageItem } from "../plugins/interfaces";
// import "./styles.scss";
export const ChatConfirmDeleteModal = () => {
const [isOpen, setIsOpen] = useState(false);
const [item, setItem] = useState<IChatMessage>(null);
const [item, setItem] = useState<IChatMessage | Partial<IChatMessageItem>[]>(
null
);
const [isDeleteForAll, setDeleteForAll] = useState<boolean>(false);
const [actions, setActions] = useState(null);
const toogle = () => setIsOpen((prev) => !prev);
@ -17,6 +22,8 @@ export const ChatConfirmDeleteModal = () => { @@ -17,6 +22,8 @@ export const ChatConfirmDeleteModal = () => {
setItem(payload.message);
setIsOpen(payload.isShow);
setDeleteForAll(payload.deleteForAll);
if (payload.onSuccess) setActions({ onSuccess: payload.onSuccess });
},
[setItem, setIsOpen]
);
@ -31,6 +38,7 @@ export const ChatConfirmDeleteModal = () => { @@ -31,6 +38,7 @@ export const ChatConfirmDeleteModal = () => {
const onConfirm = async () => {
await chatMessagesService.deleteChatMessage(item, isDeleteForAll);
if (actions?.onSuccess) actions?.onSuccess();
toogle();
};

14
src/containers/Chats/plugins/chat-header.component.tsx

@ -1,7 +1,11 @@ @@ -1,7 +1,11 @@
import React, { FC, useState } from "react";
import { Tooltip } from "antd";
import { hasImageUrl, IChatDetails, IconComponent } from "@/shared";
import { ChatAvatarWithOnlineIndicator, ChatHeaderInfo } from "../atoms";
import {
ChatAvatarWithOnlineIndicator,
ChatHeaderInfo,
SelectedMessagesInfo,
} from "../atoms";
import "./style.scss";
import gearIcon from "@/assets/img/gear-icon.svg";
import chatUnreadIcon from "@/assets/img/chat-unread-icon.svg";
@ -15,9 +19,7 @@ interface ChatHeaderProps { @@ -15,9 +19,7 @@ interface ChatHeaderProps {
isUnread: boolean;
setPinned: () => void | Promise<void>;
setUnread: () => void | Promise<void>;
// unreadMessagesCount: number;
// sendDateTime: string;
// onPress: () => void;
onPressActionsBtn: () => void;
}
export const ChatHeader: FC<ChatHeaderProps> = ({
@ -29,6 +31,7 @@ export const ChatHeader: FC<ChatHeaderProps> = ({ @@ -29,6 +31,7 @@ export const ChatHeader: FC<ChatHeaderProps> = ({
isUnread,
setPinned,
setUnread,
onPressActionsBtn,
}) => {
const [isSettingsOpen, setSettingsOpen] = useState<boolean>(false);
@ -51,6 +54,9 @@ export const ChatHeader: FC<ChatHeaderProps> = ({ @@ -51,6 +54,9 @@ export const ChatHeader: FC<ChatHeaderProps> = ({
/>
<ChatHeaderInfo label={label} userCount={userCount} />
</div>
<SelectedMessagesInfo onPressActionsBtn={onPressActionsBtn} />
<div className="chat-header-settings">
{!isUnread && (
<Tooltip

8
src/containers/Chats/plugins/chat-item-file.component.tsx

@ -11,6 +11,8 @@ import { ChatItem } from "./chat-item.component"; @@ -11,6 +11,8 @@ import { ChatItem } from "./chat-item.component";
import { IChatMessage } from "./interfaces";
import { ShowDocModal } from "../components";
import { useChatViewModeState } from "../hooks";
import { ChatViewModeEnum } from "../enums";
interface ChatItemFileProps extends IChatMessage {
onMenuPress?: () => void;
@ -22,6 +24,8 @@ interface ChatItemFileProps extends IChatMessage { @@ -22,6 +24,8 @@ interface ChatItemFileProps extends IChatMessage {
}
export const ChatItemFile: FC<ChatItemFileProps> = (props) => {
const viewMode = useChatViewModeState((s) => s.mode);
const [isOpenDocModal, setOpenDocModal] = useState<boolean>(false);
const iconName = getIconNameByExtension(props?.content?.fileUrl);
@ -48,7 +52,9 @@ export const ChatItemFile: FC<ChatItemFileProps> = (props) => { @@ -48,7 +52,9 @@ export const ChatItemFile: FC<ChatItemFileProps> = (props) => {
<ChatItem {...props}>
<div
className="chat-item-file-container"
onClick={() => setOpenDocModal(true)}
onClick={() => {
if (viewMode === ChatViewModeEnum.DEFAULT) setOpenDocModal(true);
}}
>
<div className="file-icon-container">
<IconComponent className="file-item-icon" name={iconName} />

7
src/containers/Chats/plugins/chat-item-image.component.tsx

@ -3,6 +3,8 @@ import React, { FC, useEffect, useState } from "react"; @@ -3,6 +3,8 @@ import React, { FC, useEffect, useState } from "react";
import { IMessage } from "@/shared";
import { ChatItem } from "./chat-item.component";
import { ShowImageModal } from "../components";
import { useChatViewModeState } from "../hooks";
import { ChatViewModeEnum } from "../enums";
interface ChatItemImageProps extends IMessage {
onMenuPress?: () => void;
@ -14,6 +16,8 @@ interface ChatItemImageProps extends IMessage { @@ -14,6 +16,8 @@ interface ChatItemImageProps extends IMessage {
}
export const ChatItemImage: FC<ChatItemImageProps> = (props) => {
const viewMode = useChatViewModeState((s) => s.mode);
const [isOpenImageModal, setOpenImageModal] = useState<boolean>(false);
const [imgHeight, setHeight] = useState(null);
@ -65,7 +69,8 @@ export const ChatItemImage: FC<ChatItemImageProps> = (props) => { @@ -65,7 +69,8 @@ export const ChatItemImage: FC<ChatItemImageProps> = (props) => {
<div
className="chat-item-image-container"
onClick={() => {
setOpenImageModal(true);
if (viewMode === ChatViewModeEnum.DEFAULT)
setOpenImageModal(true);
}}
>
<img

60
src/containers/Chats/plugins/chat-item-video.component.tsx

@ -1,8 +1,13 @@ @@ -1,8 +1,13 @@
import React, { FC, useMemo, useRef, useState } from "react";
import React, { FC, useCallback, useMemo, useRef, useState } from "react";
import { Dropdown, Menu } from "antd";
import { ItemType } from "antd/lib/menu/hooks/useItems";
import _ from "lodash";
import { IconComponent, MessageType, useEventsListener } from "@/shared";
import {
CheckBoxForm,
IconComponent,
MessageType,
useEventsListener,
} from "@/shared";
import ReactPlayer from "react-player/lazy";
import { IChatMessage } from "./interfaces";
import check_1 from "@/assets/img/check_1.svg";
@ -11,6 +16,8 @@ import moment from "moment"; @@ -11,6 +16,8 @@ import moment from "moment";
import { Avatar, ForwardMessageButton } from "../atoms";
import { ForwardedMessageHeader } from "./forwarded-message-header.component";
import { RepliedMessageInfo } from "./replied-message-info.component";
import { useChatSelectedMessagesState, useChatViewModeState } from "../hooks";
import { ChatViewModeEnum } from "../enums";
interface ChatItemVideoProps extends IChatMessage {
onMenuPress?: () => void;
@ -23,9 +30,14 @@ interface ChatItemVideoProps extends IChatMessage { @@ -23,9 +30,14 @@ interface ChatItemVideoProps extends IChatMessage {
export const ChatItemVideo: FC<ChatItemVideoProps> = (props) => {
// const player = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const [items, setItems] = useState([]);
const viewMode = useChatViewModeState((s) => s.mode);
const selectedMessages = useChatSelectedMessagesState((s) => s.messages);
const { selectMessage } = useChatSelectedMessagesState();
useEventsListener(
"openMessageMenuOptions",
(payload) => {
@ -34,6 +46,10 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = (props) => { @@ -34,6 +46,10 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = (props) => {
[setItems]
);
useMemo(() => {
if (viewMode === ChatViewModeEnum.SELECT) setIsPlaying(false);
}, [viewMode]);
const repliedMessage =
props.content?.replyToMessage?.type === MessageType.Forwarded
? props.content?.replyToMessage?.content?.originalMessage
@ -52,13 +68,26 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = (props) => { @@ -52,13 +68,26 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = (props) => {
);
}, [items]);
const select = () => selectMessage(props);
const handleClickMessageItem = useCallback(() => {
if (viewMode === ChatViewModeEnum.SELECT) select();
}, [viewMode]);
return (
<div
className={
"chat-item-video-container " + (props.isMy ? "my-video-container" : "")
"chat-item-video-container " +
(props.isMy && viewMode === ChatViewModeEnum.DEFAULT
? "my-video-container"
: "") +
(props.isMy && viewMode === ChatViewModeEnum.SELECT
? "mySelectableContainer"
: "")
}
onClick={handleClickMessageItem}
>
{!props.isMy ? (
{!props.isMy && viewMode === ChatViewModeEnum.DEFAULT ? (
<div className="chat-avatar-container">
<Avatar
imageUrl={props.author.avatar}
@ -68,8 +97,24 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = (props) => { @@ -68,8 +97,24 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = (props) => {
/>
</div>
) : null}
{viewMode === ChatViewModeEnum.SELECT ? (
<CheckBoxForm
className="message-checkbox"
label=""
onChange={select}
checkBoxProps={{
checked: Boolean(
_.find(selectedMessages, (it) => it.id === props.id)
),
onClick: select,
}}
/>
) : null}
<Dropdown
onVisibleChange={props.onMenuPress}
disabled={viewMode === ChatViewModeEnum.SELECT}
overlay={menu}
trigger={[`contextMenu`]}
>
@ -107,8 +152,9 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = (props) => { @@ -107,8 +152,9 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = (props) => {
<ReactPlayer
url={props.content?.fileUrl}
className="react-player"
controls={true}
playing={false}
controls={viewMode === ChatViewModeEnum.DEFAULT}
playing={isPlaying}
onPlay={() => setIsPlaying(true)}
width="100%"
height="100%"
config={{ file: { attributes: { controlsList: "nodownload" } } }}
@ -127,7 +173,7 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = (props) => { @@ -127,7 +173,7 @@ export const ChatItemVideo: FC<ChatItemVideoProps> = (props) => {
</div>
</div>
</Dropdown>
{!props.isMy && (
{!props.isMy && viewMode === ChatViewModeEnum.DEFAULT && (
<>
<ForwardMessageButton onPress={props.onForwardPressMessage} />
</>

57
src/containers/Chats/plugins/chat-item.component.tsx

@ -1,8 +1,9 @@ @@ -1,8 +1,9 @@
import React, { FC, useMemo, useState } from "react";
import React, { FC, useCallback, useMemo, useState } from "react";
import { Dropdown, Menu } from "antd";
import { ItemType } from "antd/lib/menu/hooks/useItems";
import _ from "lodash";
import {
CheckBoxForm,
IconComponent,
IMessage,
MessageType,
@ -15,6 +16,9 @@ import check_1 from "@/assets/img/check_1.svg"; @@ -15,6 +16,9 @@ import check_1 from "@/assets/img/check_1.svg";
import checks_1 from "@/assets/img/checks_1.svg";
import { ForwardedMessageHeader } from "./forwarded-message-header.component";
import { RepliedMessageInfo } from "./replied-message-info.component";
import { useChatViewModeState } from "../hooks";
import { ChatViewModeEnum } from "../enums";
import { useChatSelectedMessagesState } from "../hooks/use-selected-messages.hook";
interface ChatItemProps extends IMessage {
containerStyle?: string;
@ -37,6 +41,22 @@ export const ChatItem: FC<ChatItemProps> = (props) => { @@ -37,6 +41,22 @@ export const ChatItem: FC<ChatItemProps> = (props) => {
const [innerVisible, setInnerVisible] = useState(false);
const [linkValue, setLinkValue] = useState<string>("");
const viewMode = useChatViewModeState((s) => s.mode);
const selectedMessages = useChatSelectedMessagesState((s) => s.messages);
const { selectMessage } = useChatSelectedMessagesState();
const isSelectable = useMemo(() => {
if (viewMode === ChatViewModeEnum.DEFAULT) return false;
return (
props.type === MessageType.Text ||
props.type === MessageType.Image ||
props.type === MessageType.Video ||
props.type === MessageType.Audio ||
props.type === MessageType.File ||
props.type === MessageType.Sticker
);
}, [viewMode, props.type]);
const repliedMessage =
props.content?.replyToMessage?.type === MessageType.Forwarded
? props.content?.replyToMessage?.content?.originalMessage
@ -113,6 +133,7 @@ export const ChatItem: FC<ChatItemProps> = (props) => { @@ -113,6 +133,7 @@ export const ChatItem: FC<ChatItemProps> = (props) => {
};
const onContextMenu = (event) => {
if (isSelectable) return;
closeOtherMenu();
if (event.target.id === "link") {
const linkValue = (event.target as Element).getAttribute("href");
@ -131,14 +152,25 @@ export const ChatItem: FC<ChatItemProps> = (props) => { @@ -131,14 +152,25 @@ export const ChatItem: FC<ChatItemProps> = (props) => {
setInnerVisible(false);
});
const select = () => selectMessage(props);
const handleClickMessageItem = useCallback(() => {
setVisible(false);
if (isSelectable) select();
}, [isSelectable]);
return (
<div
className={"chat-item-container " + (props.isMy ? "myContainer" : "")}
className={
"chat-item-container " +
(props.isMy && !isSelectable ? "myContainer" : "") +
(props.isMy && isSelectable ? "mySelectableContainer" : "")
}
id="message"
onClick={() => setVisible(false)}
onClick={handleClickMessageItem}
onContextMenu={onContextMenu}
>
{!props.isMy ? (
{!props.isMy && !isSelectable ? (
<div className="chat-avatar-container">
<Avatar
onClick={props.onProfilePress}
@ -148,6 +180,21 @@ export const ChatItem: FC<ChatItemProps> = (props) => { @@ -148,6 +180,21 @@ export const ChatItem: FC<ChatItemProps> = (props) => {
/>
</div>
) : null}
{isSelectable ? (
<CheckBoxForm
className="message-checkbox"
label=""
onChange={select}
checkBoxProps={{
checked: Boolean(
_.find(selectedMessages, (it) => it.id === props.id)
),
onClick: select,
}}
/>
) : null}
<Dropdown
onVisibleChange={props.onMenuPress}
visible={_.isEmpty(items) ? false : visible}
@ -252,7 +299,7 @@ export const ChatItem: FC<ChatItemProps> = (props) => { @@ -252,7 +299,7 @@ export const ChatItem: FC<ChatItemProps> = (props) => {
</div>
</div>
</Dropdown>
{!props.isMy && (
{!props.isMy && !isSelectable && (
<>
<ForwardMessageButton onPress={props.onForwardPressMessage} />
</>

1
src/containers/Chats/plugins/interfaces.ts

@ -3,6 +3,7 @@ import { MessageType } from "@/shared/enums"; @@ -3,6 +3,7 @@ import { MessageType } from "@/shared/enums";
export interface IChatMessage {
id: number;
content: any;
chatId: number;
authorId?: number;
readByUsersId?: number[];
author?: {

62
src/containers/Chats/plugins/style.scss

@ -33,6 +33,14 @@ $bg-color-lighter: rgba(158, 39, 67, 0.1); @@ -33,6 +33,14 @@ $bg-color-lighter: rgba(158, 39, 67, 0.1);
justify-content: flex-end;
}
.chat-item-container.mySelectableContainer {
justify-content: space-between;
}
.chat-item-video-container.mySelectableContainer {
justify-content: space-between;
}
.chat-item-container:hover > div {
display: block;
}
@ -955,3 +963,57 @@ $bg-color-lighter: rgba(158, 39, 67, 0.1); @@ -955,3 +963,57 @@ $bg-color-lighter: rgba(158, 39, 67, 0.1);
}
}
}
.message-checkbox {
align-self: center;
margin-right: 19px;
&:hover {
.ant-checkbox-inner {
border: 1px solid #e2e8f0;
background-color: #f8f8f8;
}
}
& .ant-checkbox-input:focus + .ant-checkbox-inner {
border-color: #e2e8f0 !important;
}
.ant-checkbox-inner {
width: 20px;
height: 20px;
border: 1px solid #e2e8f0;
background-color: #f8f8f8;
:hover {
border: 1px solid #e2e8f0;
background-color: #f8f8f8;
}
::after {
border-radius: 50% !important;
}
}
.ant-checkbox-checked {
& .ant-checkbox-input:focus + .ant-checkbox-inner {
border-color: #9e2743 !important;
}
&::after {
border: none !important;
}
::after {
left: 4.7px;
top: 7.8px;
}
}
}
.custom-info-message {
.ant-message-notice-content {
border-radius: 4px;
filter: drop-shadow(rgb(224, 224, 224) 2px 2px 4px);
}
}

17
src/containers/Chats/smart-components/chats-select-list.smart-component.tsx

@ -8,9 +8,10 @@ import React, { FC } from "react"; @@ -8,9 +8,10 @@ import React, { FC } from "react";
import { useHistory } from "react-router-dom";
import { ChatsSelectListWithSearch } from "../components";
import { useSelectedChats } from "../hooks";
import { IChatMessage } from "../plugins/interfaces";
interface IProps {
item: IMessage;
item: IMessage | IChatMessage[];
}
export const ChatsSelectListSmart: FC<IProps> = ({ item }) => {
@ -30,11 +31,17 @@ export const ChatsSelectListSmart: FC<IProps> = ({ item }) => { @@ -30,11 +31,17 @@ export const ChatsSelectListSmart: FC<IProps> = ({ item }) => {
const chatsIds = selectedChats.map((item) => item.id);
if (_.isEmpty(chatsIds)) return;
const messages = _.isArray(item)
? (item as IChatMessage[])
: [item as IMessage];
try {
await chatMessagesApi.forwardMessageReq({
messageId: item.id,
chatsIds: chatsIds,
});
for await (const message of messages) {
await chatMessagesApi.forwardMessageReq({
messageId: message.id,
chatsIds: chatsIds,
});
}
appEvents.emit("closeForwardMessageModal", { isShow: false });

3
src/containers/Chats/transforms/chat-messages.transforms.ts

@ -176,11 +176,12 @@ export const transformMessage = ( @@ -176,11 +176,12 @@ export const transformMessage = (
return {
id: item.id,
text: item.content?.message,
chatId: item.chatId,
createdAt: item.createdAt,
authorId: item.userId,
author: {
id: author?.userId,
name: item.userId !== accountId && fullName,
name: fullName,
avatar:
item.userId !== accountId &&
hasImageUrl(author?.user?.avatarUrl, fullName),

22
src/services/domain/chat-messages.service.ts

@ -15,6 +15,8 @@ import { IChatMessage, runActionByType } from "@/shared"; @@ -15,6 +15,8 @@ import { IChatMessage, runActionByType } from "@/shared";
import { appEvents } from "@/shared/events";
import { SetUnreadMessagesCount } from "@/store/chats";
import { simpleDispatch } from "@/store/store-helpers";
import { IChatMessage as IChatMessageItem } from "@/containers/Chats/plugins/interfaces";
import _ from "lodash";
const fetchMessages = async (params: {
params: IFetchChatMessages;
@ -66,6 +68,7 @@ const sendMediaMessage = async (data: ISendFileMessage[]) => { @@ -66,6 +68,7 @@ const sendMediaMessage = async (data: ISendFileMessage[]) => {
});
}
};
const sendFileMessage = async (data: ISendFilesMessages) => {
try {
await chatMessagesApi.sendFileMessageReq(data);
@ -74,20 +77,27 @@ const sendFileMessage = async (data: ISendFilesMessages) => { @@ -74,20 +77,27 @@ const sendFileMessage = async (data: ISendFilesMessages) => {
console.log("SEND FILE MESSAGE ERROR", err.response.data);
}
};
const deleteChatMessage = async (
message: IChatMessage,
message: IChatMessage | Partial<IChatMessageItem>[],
deleteForAll?: boolean
) => {
const params: IDeleteMessageParams = {};
if (deleteForAll) params.deleteForAll = deleteForAll;
const messages = _.isArray(message)
? (message as IChatMessageItem[])
: [message as IChatMessage];
try {
await chatMessagesApi.deleteMessageByIdReq(message.id, params);
for await (const item of messages) {
await chatMessagesApi.deleteMessageByIdReq(item.id, params);
appEvents.emit("onDeleteMessage", {
chatId: message.chatId,
messageId: message.id,
});
appEvents.emit("onDeleteMessage", {
chatId: item.chatId,
messageId: item.id,
});
}
} catch (err) {
console.log("ERROR ON DELETE MESSAGE", err);
}

9
src/shared/components/form/CheckBox.tsx

@ -1,18 +1,21 @@ @@ -1,18 +1,21 @@
import React, { PureComponent } from "react";
import React from "react";
import { Checkbox } from "antd";
import { UseFormRegisterReturn } from "react-hook-form";
import { CheckboxProps } from "antd/lib/checkbox/Checkbox";
interface IProps {
label: string;
onChange?: (v: any) => any;
field?: any;
className?: string;
checkBoxProps?: CheckboxProps;
}
export const CheckBoxForm = (props: IProps) => {
return (
<Checkbox
// style={{ borderColor "rgba(158, 39, 67, 1)" }}
className={props.className}
onChange={(v) => props.onChange(v.target.checked)}
{...props.checkBoxProps}
{...props?.field}
>
{props.label}

15
src/shared/events/index.ts

@ -6,6 +6,7 @@ import { @@ -6,6 +6,7 @@ import {
} from "../interfaces";
import { Events } from "jet-tools";
import { ChatMemberRole, EntityType } from "../enums";
import { IChatMessage as IMessageInChat } from "@/containers/Chats/plugins/interfaces";
export type AppEvents = {
onDeleteChatForMe: { chatId: number };
@ -27,7 +28,7 @@ export type AppEvents = { @@ -27,7 +28,7 @@ export type AppEvents = {
onReadDocuments: { taskId: number };
openForwardMessageModal: {
message: IMessage;
message: IMessage | IMessageInChat[];
isShow: boolean;
onCancel?: () => void;
};
@ -41,9 +42,10 @@ export type AppEvents = { @@ -41,9 +42,10 @@ export type AppEvents = {
};
openConfirmDeleteMessageModal: {
message: IChatMessage;
message: IChatMessage | Partial<IMessageInChat>[];
deleteForAll: boolean;
isShow: boolean;
onSuccess?: () => void;
};
closeConfirmDeleteMessageModal: {
@ -59,6 +61,15 @@ export type AppEvents = { @@ -59,6 +61,15 @@ export type AppEvents = {
}>;
onCancel?: () => void;
};
openSelectedMessagesMenuOptions: {
items: Array<{
name: string;
onClick: () => void | Promise<void>;
}>;
onCancel?: () => void;
};
selectSticker: {
onSelect: (sticker: string) => void;
};

1
src/shared/helpers/index.ts

@ -17,3 +17,4 @@ export * from "./tranposition-cipher.helper"; @@ -17,3 +17,4 @@ export * from "./tranposition-cipher.helper";
export * from "./transforms.helpers";
export * from "./url.helpers";
export * from "./user.helpers";
export * from "./title-by-count.helper";

17
src/shared/helpers/title-by-count.helper.ts

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
export const getTitleByCount = (
count: number,
[form1, form2, form3]: string[],
) => {
const tens = Math.abs(count) % 100
const units = tens % 10
if (tens > 10 && tens < 20) {
return `${count} ${form3}`
}
if (units > 1 && units < 5) {
return `${count} ${form2}`
}
if (units == 1) {
return `${count} ${form1}`
}
return `${count} ${form3}`
}

1
src/shared/interfaces/messages.interfaces.ts

@ -5,6 +5,7 @@ export interface IMessage { @@ -5,6 +5,7 @@ export interface IMessage {
content: any;
authorId?: number;
readByUsersId?: number[];
chatId: number;
author?: {
name: string;
id: number;

Loading…
Cancel
Save