diff --git a/packages/extension/src/@types/chat.ts b/packages/extension/src/@types/chat.ts new file mode 100644 index 0000000000..5f16d82807 --- /dev/null +++ b/packages/extension/src/@types/chat.ts @@ -0,0 +1,132 @@ +// Params Type Definitions + +export interface NewGroupDetails { + isEditGroup: boolean; + group: GroupDetails; +} +export interface GroupDetails { + contents: string; + description: string; + groupId: string; + members: GroupMembers[]; + name: string; + onlyAdminMessages: boolean; +} + +export interface GroupMembers { + address: string; + pubKey: string; + encryptedSymmetricKey: string; + isAdmin: boolean; +} + +export interface GroupMessagePayload { + message: string; + type: string; +} + +export interface PublicKeyDetails { + address: string; + channelId: string; + privacySetting: string; + publicKey: string; +} + +export interface NewMessageUpdate { + type: string; + message: Message; +} + +// Graphql Type Definitions +export interface Message { + id: string; + sender: string; + target: string; + contents: string; + groupId: string; + expiryTimestamp: string; + commitTimestamp: string; +} + +export interface GroupAddress { + address: string; + pubKey: string; + lastSeenTimestamp: string; + groupLastSeenTimestamp: string; + encryptedSymmetricKey: string; + isAdmin: boolean; + removedAt: Date; +} + +export interface Group { + id: string; // groupID + name: string; // contactAddress + isDm: boolean; + addresses: GroupAddress[]; + lastMessageContents: string; + lastMessageSender: string; + lastMessageTimestamp: string; + lastSeenTimestamp: string; + description?: string; + createdAt: string; + removedAt: Date; +} + +export interface Pagination { + page: number; + pageCount: number; + total: number; + lastPage: number; +} + +//Redux Selectors Type Definitions +export interface Messages { + [key: string]: Message; +} + +export interface Chat { + contactAddress: string; + messages: Messages; + pubKey?: string; + pagination: Pagination; +} + +//key is group ID +export interface Chats { + [key: string]: Chat; +} + +export interface BlockedAddressState { + [key: string]: boolean; +} + +export interface Groups { + [contactAddress: string]: Group; +} + +export interface NameAddress { + [key: string]: string; +} + +export enum GroupChatOptions { + groupInfo, + muteGroup, + leaveGroup, + deleteGroup, + chatSettings, +} + +export enum GroupChatMemberOptions { + addToAddressBook, + viewInAddressBook, + messageMember, + removeMember, + removeAdminStatus, + makeAdminStatus, + dissmisPopup, +} + +export enum CommonPopupOptions { + cancel, + ok, +} diff --git a/packages/extension/src/bootstrap.tsx b/packages/extension/src/bootstrap.tsx index d664ac5325..dd2edaf216 100644 --- a/packages/extension/src/bootstrap.tsx +++ b/packages/extension/src/bootstrap.tsx @@ -1,14 +1,14 @@ import React, { Suspense } from "react"; import ReactDOM from "react-dom"; -import { Banner } from "./components/banner"; +import { Banner } from "@components/banner"; const Application = React.lazy(() => import("./index")); const LoadingScreen: React.FC = () => { return ( ); }; diff --git a/packages/extension/src/components/chat-actions-dropdown/index.tsx b/packages/extension/src/components/chat-actions-dropdown/index.tsx new file mode 100644 index 0000000000..93f7389ba6 --- /dev/null +++ b/packages/extension/src/components/chat-actions-dropdown/index.tsx @@ -0,0 +1,101 @@ +import amplitude from "amplitude-js"; +import React from "react"; +import { useHistory } from "react-router"; +import style from "./style.module.scss"; + +export const ChatActionsDropdown = ({ + added, + blocked, + showDropdown, + handleClick, +}: { + added: boolean; + blocked: boolean; + showDropdown: boolean; + handleClick: (data: string) => void; +}) => { + return ( + <> + {showDropdown && ( +
+ {added ? : } + {blocked ? ( + + ) : ( + + )} + {/*
handleClick("delete")}>Delete chat
*/} +
+ )} + + ); +}; + +const ViewContactOption = () => { + const history = useHistory(); + return ( +
{ + amplitude.getInstance().logEvent("Address book viewed", {}); + history.push("/setting/address-book"); + }} + > + View in address book +
+ ); +}; + +const AddContactOption = () => { + const history = useHistory(); + const userName = history.location.pathname.split("/")[2]; + return ( +
{ + amplitude.getInstance().logEvent("Add to address click", {}); + history.push({ + pathname: "/setting/address-book", + state: { + openModal: true, + addressInputValue: userName, + }, + }); + }} + > + Add to address book +
+ ); +}; + +const BlockOption = ({ + handleClick, +}: { + handleClick: (data: string) => void; +}) => { + return ( +
{ + amplitude.getInstance().logEvent("Block click", {}); + handleClick("block"); + }} + > + Block contact +
+ ); +}; + +const UnblockOption = ({ + handleClick, +}: { + handleClick: (data: string) => void; +}) => { + return ( +
{ + amplitude.getInstance().logEvent("Unblock click", {}); + handleClick("unblock"); + }} + > + Unblock contact +
+ ); +}; diff --git a/packages/extension/src/pages/chatSection/style.module.scss b/packages/extension/src/components/chat-actions-dropdown/style.module.scss similarity index 100% rename from packages/extension/src/pages/chatSection/style.module.scss rename to packages/extension/src/components/chat-actions-dropdown/style.module.scss diff --git a/packages/extension/src/components/chat-actions-popup/alert-popup.tsx b/packages/extension/src/components/chat-actions-popup/alert-popup.tsx new file mode 100644 index 0000000000..f01fe02c01 --- /dev/null +++ b/packages/extension/src/components/chat-actions-popup/alert-popup.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import style from "./style.module.scss"; +import { CommonPopupOptions } from "@chatTypes"; +export const AlertPopup = ({ + heading, + description, + firstButtonTitle, + secondButtonTitle, + processing, + onClick, +}: { + setConfirmAction: React.Dispatch>; + heading: string; + description: string; + firstButtonTitle: string; + secondButtonTitle: string; + processing?: boolean; + onClick: (option: CommonPopupOptions) => void; +}) => { + return ( + <> +
+
+

{heading}

+
+

+ {description} +

+
+
+ + +
+
+ + ); +}; diff --git a/packages/extension/src/pages/chatSection/block-user-popup.tsx b/packages/extension/src/components/chat-actions-popup/block-user-popup.tsx similarity index 96% rename from packages/extension/src/pages/chatSection/block-user-popup.tsx rename to packages/extension/src/components/chat-actions-popup/block-user-popup.tsx index 14176cd6da..28744be28d 100644 --- a/packages/extension/src/pages/chatSection/block-user-popup.tsx +++ b/packages/extension/src/components/chat-actions-popup/block-user-popup.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useHistory } from "react-router"; -import { blockUser } from "../../graphQL/messages-api"; +import { blockUser } from "@graphQL/messages-api"; import style from "./style.module.scss"; export const BlockUserPopup = ({ diff --git a/packages/extension/src/pages/chatSection/delete-chat-popup.tsx b/packages/extension/src/components/chat-actions-popup/delete-chat-popup.tsx similarity index 100% rename from packages/extension/src/pages/chatSection/delete-chat-popup.tsx rename to packages/extension/src/components/chat-actions-popup/delete-chat-popup.tsx diff --git a/packages/extension/src/pages/setting/chat/block/unblock-user-popup.tsx b/packages/extension/src/components/chat-actions-popup/delete-group-popup.tsx similarity index 58% rename from packages/extension/src/pages/setting/chat/block/unblock-user-popup.tsx rename to packages/extension/src/components/chat-actions-popup/delete-group-popup.tsx index c58ffb49e0..edf336333d 100644 --- a/packages/extension/src/pages/setting/chat/block/unblock-user-popup.tsx +++ b/packages/extension/src/components/chat-actions-popup/delete-group-popup.tsx @@ -1,26 +1,23 @@ +import { deleteGroup } from "@graphQL/groups-api"; +import amplitude from "amplitude-js"; import React, { useState } from "react"; -import { unblockUser } from "../../../../graphQL/messages-api"; +import { useHistory } from "react-router"; import style from "./style.module.scss"; -export const UnblockUserPopup = ({ - userName, +export const DeleteGroupPopup = ({ setConfirmAction, }: { - userName: string; setConfirmAction: React.Dispatch>; }) => { const [processing, setProcessing] = useState(false); - - const handleBlock = async () => { + const history = useHistory(); + const handleDelete = async () => { setProcessing(true); - try { - await unblockUser(userName); - } catch (e) { - console.log(e); - } finally { - setProcessing(false); - setConfirmAction(false); - } + const groupId = history.location.pathname.split("/")[3]; + deleteGroup(groupId); + setConfirmAction(false); + amplitude.getInstance().logEvent("Delete group click", {}); + history.push("/chat"); }; const handleCancel = () => { @@ -31,11 +28,11 @@ export const UnblockUserPopup = ({ <>
-

Unblock User

+

Delete Group

- This contact will not be able to send you messages. The contact will - not be notified. + You will lose all your messages in this group. This action cannot be + undone

@@ -45,10 +42,10 @@ export const UnblockUserPopup = ({
diff --git a/packages/extension/src/components/chat-actions-popup/index.tsx b/packages/extension/src/components/chat-actions-popup/index.tsx new file mode 100644 index 0000000000..4bc692be48 --- /dev/null +++ b/packages/extension/src/components/chat-actions-popup/index.tsx @@ -0,0 +1,67 @@ +import { CommonPopupOptions } from "@chatTypes"; +import React, { useState } from "react"; +import { useHistory } from "react-router"; +import { AlertPopup } from "./alert-popup"; +import { BlockUserPopup } from "./block-user-popup"; +import { DeleteChatPopup } from "./delete-chat-popup"; +import { DeleteGroupPopup } from "./delete-group-popup"; +import { UnblockUserPopup } from "./unblock-user-popup"; + +export const ChatActionsPopup = ({ + action, + setConfirmAction, + handleAction, +}: { + action: string; + setConfirmAction: React.Dispatch>; + handleAction?: () => void; +}) => { + const [processing, setProcessing] = useState(false); + const history = useHistory(); + /// Target address for one to one chat + const targetAddress = history.location.pathname.split("/")[2]; + + const handleLeaveGroup = async () => { + setProcessing(true); + if (handleAction) handleAction(); + }; + + return ( + <> + {action === "block" && ( + + )} + {action === "unblock" && ( + + )} + {action === "delete" && ( + + )} + {action === "deleteGroup" && ( + + )} + {action === "leaveGroup" && ( + { + if (action === CommonPopupOptions.ok) { + handleLeaveGroup(); + } else { + setConfirmAction(false); + } + }} + /> + )} + + ); +}; diff --git a/packages/extension/src/components/chat-actions-popup/style.module.scss b/packages/extension/src/components/chat-actions-popup/style.module.scss new file mode 100644 index 0000000000..4ce143b947 --- /dev/null +++ b/packages/extension/src/components/chat-actions-popup/style.module.scss @@ -0,0 +1,301 @@ +.username { + margin-left: -12px; + margin-right: -12px; + padding: 10px; + align-items: center; + display: flex; + justify-content: space-between; + background-color: #ffffff; + border-radius: 6px; + flex-direction: row; +} + +.overlay { + position: absolute; + top: 0; + left: 0; + z-index: 1; + height: 531px; + width: 100%; + background: black; + opacity: 0.5; +} + +.rightBox { + display: flex; + align-items: center; + justify-content: space-between; + .more { + float: right; + transform: rotate(90deg); + } +} +.leftBox { + display: flex; + align-items: center; + width: 100%; + .user { + word-wrap: break-word; + max-width: 300px; + text-align: left; + padding: 2px 10px; + } + .userText { + display: inline-block; + } + .copyIcon { + color: #525f7f; + cursor: pointer; + margin-left: 10px; + } +} +.messageRef { + min-height: 10px; +} +.backBtn { + flex-basis: 25px; + width: 24px; + height: 24px; +} + +.send-message-inputArea { + // height: 32px; + outline: none !important; +} + +.recieverName { + text-align: center; + font-size: 16px; + color: #525f7f; + line-height: 16px; + font-weight: 400; + position: relative; + margin-left: 5px; +} + +.send-message-icon { + margin-right: 5px; + cursor: pointer; +} + +.chatArea { + display: flex; + flex-direction: column; + height: 70vh; + .messages { + height: 63vh; + display: flex; + flex-direction: column; + overflow-y: auto; + overflow-x: clip; + margin-right: -12px; + margin-bottom: 10px; + scroll-behavior: smooth; + + p { + width: 320px; + text-align: center; + font-size: 14px; + margin: auto; + margin-top: 20px; + margin-bottom: 20px; + color: #525f7f; + } + } +} +.loader { + text-align: center; + display: flex; + justify-content: center; + align-items: center; + font-size: 12px; +} +.inputText { + // margin-left: -12px; + border: #525f7f 1px solid; + background: white; + display: flex; + flex-wrap: nowrap; + flex-direction: row; + align-items: center; + + .inputArea { + width: 330px; + padding: 4.5px 10px; + border-radius: 5px; + height: 33px; + resize: none; + border: 0px; + } + + .inputArea:focus { + outline: solid #11b4ff 0.5px; + } +} + +.enterMessageIcon { + // border: 1px solid #cad1d7; + // border-radius: 0.375rem; + padding-left: 10px; + // padding-top: 4px; + // width: 20px; + // height: 20px; + // transform: scale(1.25); +} + +.newUserText { + margin-bottom: 5px; + // position: absolute; + // background-color: #F2F3F6; + width: inherit; + + p { + font-size: 14px; + color: #808da0; + text-align: center; + } + + .buttons { + display: flex; + flex-direction: row; + justify-content: center; + + button { + padding: 15px 20px; + margin-right: 5px; + border: solid #11b4ff 1px; + border-radius: 5px; + color: #11b4ff; + font-weight: bold; + cursor: pointer; + } + } +} + +.dropdown { + position: absolute; + border: solid #dee2e6 1px; + background-color: #ffffff; + border-radius: 5px; + width: 180px; + padding: 5px; + margin-left: 160px; + top: 105px; + z-index: 2; + cursor: pointer; + + button { + border: none; + padding: 5px; + color: #343a40; + font-size: 14px; + background-color: #ffffff; + cursor: pointer; + } + + div { + padding: 5px; + } + + div:hover { + background-color: #c9d4e4; + } +} + +.popup { + width: 310px; + position: absolute; + border: solid #dee2e6 1px; + background-color: #ffffff; + margin: 20px 15px; + border-radius: 15px; + padding-bottom: 10px; + cursor: pointer; + top: 30%; + z-index: 2; + // display: flex; + // flex-direction: column; + // padding: 0px 20px; + h4 { + // border-bottom: solid #DEE2E6 2px; + padding: 0 20px 10px 20px; + color: #525f7f; + margin: 10px 0 0 0; + font-size: 18px; + } + + .textContainer { + text-align: left; + padding: 0 15px; + font-size: 14px; + color: #808da0; + width: 280px; + + label { + margin-left: 10px; + } + } + + .header { + display: flex; + flex-direction: row; + justify-content: space-between; + border-bottom: solid #dee2e6 2px; + align-items: center; + + img { + width: 8px; + height: 8px; + margin-right: 20px; + cursor: pointer; + } + } +} + +.buttonContainer { + display: flex; + flex-direction: row; + justify-content: flex-end; + padding: 10px; + border-top: solid #dee2e6 2px; + + button { + margin-left: 8px; + padding: 5px 10px; + border: solid #3b82f6 1px; + border-radius: 5px; + color: #3b82f6; + background-color: #ffffff; + cursor: pointer; + } + + .btn { + color: #ffffff; + background-color: #3b82f6; + } +} +.contactsContainer { + .displayText { + text-align: center; + font-size: 14px; + margin: auto; + margin-top: 10px; + margin-bottom: 10px; + color: #525f7f; + } + .buttons { + display: flex; + justify-content: space-evenly; + align-items: center; + margin: 0 auto; + } + button { + border: 1px solid #3b82f6; + padding: 4px 15px; + background-color: #fff; + border-radius: 4px; + color: #3b82f6; + cursor: pointer; + font-weight: 600; + } +} diff --git a/packages/extension/src/pages/chatSection/unblock-user-popup.tsx b/packages/extension/src/components/chat-actions-popup/unblock-user-popup.tsx similarity index 87% rename from packages/extension/src/pages/chatSection/unblock-user-popup.tsx rename to packages/extension/src/components/chat-actions-popup/unblock-user-popup.tsx index ed1b7a4115..619cc11ca4 100644 --- a/packages/extension/src/pages/chatSection/unblock-user-popup.tsx +++ b/packages/extension/src/components/chat-actions-popup/unblock-user-popup.tsx @@ -1,16 +1,16 @@ import React, { useState } from "react"; -import { useHistory } from "react-router"; -import { unblockUser } from "../../graphQL/messages-api"; +import { unblockUser } from "@graphQL/messages-api"; import style from "./style.module.scss"; export const UnblockUserPopup = ({ + userName, setConfirmAction, }: { + userName: string; setConfirmAction: React.Dispatch>; }) => { const [processing, setProcessing] = useState(false); - const history = useHistory(); - const userName = history.location.pathname.split("/")[2]; + const handleUnblock = async () => { setProcessing(true); try { diff --git a/packages/extension/src/components/chat-error-popup/index.tsx b/packages/extension/src/components/chat-error-popup/index.tsx index c270806bb9..1152f5573b 100644 --- a/packages/extension/src/components/chat-error-popup/index.tsx +++ b/packages/extension/src/components/chat-error-popup/index.tsx @@ -1,11 +1,8 @@ import React, { useEffect, useState } from "react"; import style from "./style.module.scss"; import { useSelector } from "react-redux"; -import { - setMessageError, - userMessagesError, -} from "../../chatStore/messages-slice"; -import { store } from "../../chatStore"; +import { setMessageError, userMessagesError } from "@chatStore/messages-slice"; +import { store } from "@chatStore/index"; export const ChatErrorPopup = () => { const errorMessage = useSelector(userMessagesError); diff --git a/packages/extension/src/components/chat-loader/index.tsx b/packages/extension/src/components/chat-loader/index.tsx index 7a95d85cb4..cc11087c64 100644 --- a/packages/extension/src/components/chat-loader/index.tsx +++ b/packages/extension/src/components/chat-loader/index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import loadingChatGif from "../../public/assets/chat-loading.gif"; +import loadingChatGif from "@assets/chat-loading.gif"; export const ChatLoader = ({ message }: { message: string }) => { return ( diff --git a/packages/extension/src/components/chat-member/index.tsx b/packages/extension/src/components/chat-member/index.tsx new file mode 100644 index 0000000000..19ea11d782 --- /dev/null +++ b/packages/extension/src/components/chat-member/index.tsx @@ -0,0 +1,118 @@ +import { fromBech32 } from "@cosmjs/encoding"; +import jazzicon from "@metamask/jazzicon"; +import React, { ReactElement, useEffect, useState } from "react"; +import ReactHtmlParser from "react-html-parser"; +import { NameAddress } from "@chatTypes"; +import { formatAddress } from "@utils/format"; +import style from "./style.module.scss"; +import classnames from "classnames"; +import { fetchPublicKey } from "@utils/fetch-public-key"; +import { userDetails } from "@chatStore/user-slice"; +import { useSelector } from "react-redux"; +import { useStore } from "../../stores"; + +export const ChatMember = (props: { + address: NameAddress; + showSelectedIcon?: boolean; + isSelected?: boolean; + isShowAdmin?: boolean; + showPointer?: boolean; + onIconClick?: VoidCallback; + onClick?: VoidCallback; +}) => { + const { name, address } = props.address; + const { + isSelected, + isShowAdmin, + showSelectedIcon = true, + showPointer = false, + onIconClick, + onClick, + } = props; + + const user = useSelector(userDetails); + const { chainStore } = useStore(); + const current = chainStore.current; + + const [isActive, setIsActive] = useState(true); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const isUserActive = async () => { + try { + const pubKey = await fetchPublicKey( + user.accessToken, + current.chainId, + address + ); + if (!pubKey || !pubKey.publicKey || !(pubKey.publicKey.length > 0)) + setIsActive(false); + } catch (e) { + console.log("NewUser/isUserActive error", e); + } finally { + setIsLoading(false); + } + }; + isUserActive(); + }, [ + address, + user.accessToken, + user.messagingPubKey.privacySetting, + user.messagingPubKey.chatReadReceiptSetting, + current.chainId, + ]); + + function decideIconLabelView(): ReactElement { + if (isLoading) { + return ; + } + + if (isActive && isShowAdmin) { + return
Admin
; + } + + if (isActive && showSelectedIcon) { + return ( +
+