Add no community chat (#209)

This commit is contained in:
Szymon Szlachtowicz 2022-01-31 09:10:19 +01:00 committed by GitHub
parent 21e49cb7bf
commit 180b2be276
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 949 additions and 253 deletions

View File

@ -103,11 +103,6 @@ export function ChatCreation({
value={query}
onInput={(e) => setQuery(e.currentTarget.value)}
/>
<SearchBlock
query={query}
discludeList={styledGroup}
onClick={addMember}
/>
</SearchMembers>
)}
{!narrow && styledGroup.length === 5 && (
@ -132,12 +127,18 @@ export function ChatCreation({
Confirm
</CreationBtn>
{!narrow && <ActivityButton className="creation" />}
<SearchBlock
query={query}
discludeList={styledGroup}
onClick={addMember}
/>
</CreationBar>
{!setEditGroup && !query && (
{!setEditGroup && (
<Contacts>
<ContactsHeading>Contacts</ContactsHeading>
<ContactsList>
{identity &&
!query &&
Object.values(contacts)
.filter(
(e) =>
@ -181,7 +182,7 @@ const CreationBar = styled.div`
display: flex;
align-items: center;
margin-bottom: 32px;
position: relative;
&.limit {
align-items: flex-start;
}

View File

@ -1,36 +0,0 @@
import React from "react";
import { ChatStateProvider } from "../contexts/chatStateProvider";
import { IdentityProvider } from "../contexts/identityProvider";
import { MessengerProvider } from "../contexts/messengerProvider";
import {
UserCreationState,
useUserCreationState,
} from "../contexts/userCreationStateProvider";
import { Chat } from "./Chat";
import { IdentityLoader } from "./Form/IdentityLoader";
type ChatLoaderProps = {
communityKey: string;
};
export function ChatLoader({ communityKey }: ChatLoaderProps) {
const [userCreationState] = useUserCreationState();
if (userCreationState === UserCreationState.NotCreating)
return (
<IdentityProvider>
<MessengerProvider communityKey={communityKey}>
<ChatStateProvider>
<Chat />
</ChatStateProvider>
</MessengerProvider>
</IdentityProvider>
);
if (userCreationState === UserCreationState.Creating) {
return <IdentityLoader />;
}
return null;
}

View File

@ -4,17 +4,19 @@ import styled from "styled-components";
import { ConfigType } from "..";
import { ActivityProvider } from "../contexts/activityProvider";
import { ChatStateProvider } from "../contexts/chatStateProvider";
import { ConfigProvider } from "../contexts/configProvider";
import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider";
import { IdentityProvider } from "../contexts/identityProvider";
import { MessengerProvider } from "../contexts/messengerProvider";
import { ModalProvider } from "../contexts/modalProvider";
import { NarrowProvider } from "../contexts/narrowProvider";
import { ToastProvider } from "../contexts/toastProvider";
import { UserCreationStateProvider } from "../contexts/userCreationStateProvider";
import { Metadata } from "../models/Metadata";
import { GlobalStyle } from "../styles/GlobalStyle";
import { Theme } from "../styles/themes";
import { ChatLoader } from "./ChatLoader";
import { Chat } from "./Chat";
interface DappConnectCommunityChatProps {
theme: Theme;
@ -37,15 +39,19 @@ export function DappConnectCommunityChat({
<FetchMetadataProvider fetchMetadata={fetchMetadata}>
<ModalProvider>
<ActivityProvider>
<UserCreationStateProvider>
<ToastProvider>
<Wrapper ref={ref}>
<GlobalStyle />
<ChatLoader communityKey={communityKey} />
<IdentityProvider>
<MessengerProvider communityKey={communityKey}>
<ChatStateProvider>
<Chat />
<div id="modal-root" />
</ChatStateProvider>
</MessengerProvider>
</IdentityProvider>
</Wrapper>
</ToastProvider>
</UserCreationStateProvider>
</ActivityProvider>
</ModalProvider>
</FetchMetadataProvider>

View File

@ -1,133 +0,0 @@
import { Identity } from "@waku/status-communities/dist/cjs";
import React, { useCallback, useEffect, useState } from "react";
import styled from "styled-components";
import { useSetIdentity } from "../../contexts/identityProvider";
import {
UserCreationState,
useUserCreationState,
} from "../../contexts/userCreationStateProvider";
import {
decryptIdentity,
loadEncryptedIdentity,
saveIdentity,
} from "../../utils";
export function IdentityLoader() {
const setIdentity = useSetIdentity();
const [password, setPassword] = useState("");
const state = useUserCreationState();
const [encryptedIdentity, setEncryptedIdentity] = useState(
loadEncryptedIdentity() ?? ""
);
const [identityInMemory, setIdentityInMemory] = useState(false);
const [wrongPassword, setWrongPassword] = useState(false);
useEffect(() => {
if (encryptedIdentity) {
setIdentityInMemory(true);
}
setWrongPassword(false);
}, [encryptedIdentity]);
const loadIdentity = useCallback(async () => {
const identity = await decryptIdentity(encryptedIdentity, password);
if (!identity) {
setWrongPassword(true);
} else {
setIdentity(identity);
state[1](UserCreationState.NotCreating);
}
}, [encryptedIdentity, password]);
const createIdentity = useCallback(async () => {
const identity = Identity.generate();
await saveIdentity(identity, password);
setIdentity(identity);
state[1](UserCreationState.NotCreating);
}, [encryptedIdentity, password]);
return (
<Wrapper>
{encryptedIdentity ? (
<FormWrappers>
<InfoDiv>Please provide password for your identity</InfoDiv>
{wrongPassword && <div>Wrong password</div>}
<Input
value={password}
type={"password"}
onChange={(e) => setPassword(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
loadIdentity();
}
}}
/>
<Button onClick={loadIdentity}>LOAD</Button>
<Button
onClick={() => {
setEncryptedIdentity("");
}}
>
Create new identity
</Button>
</FormWrappers>
) : (
<FormWrappers>
<InfoDiv>Please provide password for your identity</InfoDiv>
<Input
value={password}
type={"password"}
onChange={(e) => setPassword(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
createIdentity();
}
}}
/>
<Button onClick={createIdentity}>Create</Button>
{identityInMemory && (
<Button
onClick={() =>
setEncryptedIdentity(loadEncryptedIdentity() ?? "")
}
>
Go back
</Button>
)}
</FormWrappers>
)}
</Wrapper>
);
}
const Input = styled.input`
border-radius: 5px;
padding: 5px;
margin: 5px;
`;
const Button = styled.button`
padding: 5px;
margin: 5px;
box-shadow: 5px 5px 10px -4px rgba(197, 197, 255, 1);
border-radius: 5px;
background-color: ${({ theme }) => theme.buttonBg};
`;
const Wrapper = styled.div`
height: 100%;
width: 100%;
display: flex;
`;
const FormWrappers = styled.div`
margin: auto;
`;
const InfoDiv = styled.div`
margin: 5px;
font-weight: 500;
font-size: 16px;
`;

View File

@ -1,4 +1,3 @@
import { utils } from "@waku/status-communities/dist/cjs";
import { bufToHex } from "@waku/status-communities/dist/cjs/utils";
import React, { useMemo } from "react";
import styled from "styled-components";
@ -6,6 +5,7 @@ import styled from "styled-components";
import { useIdentity } from "../../contexts/identityProvider";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useModal } from "../../contexts/modalProvider";
import { Contact } from "../../models/Contact";
import { buttonStyles } from "../Buttons/buttonStyle";
import { LogoutIcon } from "../Icons/LogoutIcon";
import { LogoutModalName } from "../Modals/LogoutModal";
@ -15,30 +15,32 @@ import { Member } from "./Member";
export function MembersList() {
const { contacts, nickname, activeChannel } = useMessengerContext();
const identity = useIdentity();
const userPK = useMemo(
() => (identity ? bufToHex(identity?.publicKey) : undefined),
[identity]
);
const { setModal } = useModal(LogoutModalName);
const userContacts = useMemo(() => {
const members = useMemo(() => {
const contactsArray = Object.values(contacts);
if (identity) {
return Object.values(contacts).filter(
(e) => e.id != bufToHex(identity.publicKey)
);
} else {
return Object.values(contacts);
}
}, [contacts, identity]);
const members = useMemo(
() =>
if (
activeChannel &&
activeChannel?.type === "group" &&
activeChannel.members &&
identity
? activeChannel.members.filter(
(e) => e.id !== utils.bufToHex(identity.publicKey)
)
: userContacts,
[activeChannel]
);
activeChannel.type === "group" &&
activeChannel.members
) {
const returnContacts: Contact[] = [];
activeChannel.members.forEach((member) => {
if (contacts[member.id] && member.id != userPK) {
returnContacts.push(contacts[member.id]);
}
});
return returnContacts;
}
return contactsArray.filter((e) => e.id !== userPK);
}
return contactsArray;
}, [activeChannel, contacts, identity, userPK]);
const onlineContacts = useMemo(
() => members.filter((e) => e.online),
@ -57,9 +59,9 @@ export function MembersList() {
<Row>
<Member
contact={{
id: utils.bufToHex(identity.publicKey),
id: userPK ?? "",
customName: nickname,
trueName: utils.bufToHex(identity.publicKey),
trueName: userPK ?? "",
}}
isYou={true}
/>

View File

@ -18,6 +18,7 @@ const MessengerContext = createContext<MessengerType>({
communityData: undefined,
contacts: {},
contactsDispatch: () => undefined,
addContact: () => undefined,
activeChannel: undefined,
channels: {},
channelsDispatch: () => undefined,
@ -33,7 +34,7 @@ export function useMessengerContext() {
}
interface MessengerProviderProps {
communityKey: string;
communityKey: string | undefined;
children: React.ReactNode;
}

View File

@ -1,29 +0,0 @@
import React, { createContext, useContext, useState } from "react";
export enum UserCreationState {
Creating,
NotCreating,
}
type UserCreationContextType = [
UserCreationState,
React.Dispatch<React.SetStateAction<UserCreationState>>
];
const ChatStateContext = createContext<UserCreationContextType>([
UserCreationState.NotCreating,
() => undefined,
]);
export function useUserCreationState() {
return useContext(ChatStateContext);
}
export function UserCreationStateProvider({
children,
}: {
children: React.ReactNode;
}) {
const state = useState(UserCreationState.NotCreating);
return <ChatStateContext.Provider value={state} children={children} />;
}

View File

@ -0,0 +1,65 @@
import React, { useRef } from "react";
import { ThemeProvider } from "styled-components";
import styled from "styled-components";
import { ConfigType } from "..";
import { ActivityProvider } from "../contexts/activityProvider";
import { ChatStateProvider } from "../contexts/chatStateProvider";
import { ConfigProvider } from "../contexts/configProvider";
import { FetchMetadataProvider } from "../contexts/fetchMetadataProvider";
import { IdentityProvider } from "../contexts/identityProvider";
import { MessengerProvider } from "../contexts/messengerProvider";
import { ModalProvider } from "../contexts/modalProvider";
import { NarrowProvider } from "../contexts/narrowProvider";
import { ToastProvider } from "../contexts/toastProvider";
import { Metadata } from "../models/Metadata";
import { GlobalStyle } from "../styles/GlobalStyle";
import { Theme } from "../styles/themes";
import { GroupChat } from "./GroupChat";
interface DappConnectGroupChatProps {
theme: Theme;
config: ConfigType;
fetchMetadata?: (url: string) => Promise<Metadata | undefined>;
}
export function DappConnectGroupChat({
theme,
config,
fetchMetadata,
}: DappConnectGroupChatProps) {
const ref = useRef<HTMLHeadingElement>(null);
return (
<ConfigProvider config={config}>
<ThemeProvider theme={theme}>
<NarrowProvider myRef={ref}>
<FetchMetadataProvider fetchMetadata={fetchMetadata}>
<ModalProvider>
<ActivityProvider>
<ToastProvider>
<Wrapper ref={ref}>
<GlobalStyle />
<IdentityProvider>
<MessengerProvider communityKey={undefined}>
<ChatStateProvider>
<GroupChat />
<div id="modal-root" />
</ChatStateProvider>
</MessengerProvider>
</IdentityProvider>
</Wrapper>
</ToastProvider>
</ActivityProvider>
</ModalProvider>
</FetchMetadataProvider>
</NarrowProvider>
</ThemeProvider>
</ConfigProvider>
);
}
const Wrapper = styled.div`
height: 100%;
overflow: hidden;
`;

View File

@ -0,0 +1,90 @@
import React, { useState } from "react";
import styled from "styled-components";
import { Channels } from "../components/Channels/Channels";
import { ChatCreation } from "../components/Chat/ChatCreation";
import { AgreementModal } from "../components/Modals/AgreementModal";
import { CoinbaseModal } from "../components/Modals/CoinbaseModal";
import { EditModal } from "../components/Modals/EditModal";
import { LeavingModal } from "../components/Modals/LeavingModal";
import { LogoutModal } from "../components/Modals/LogoutModal";
import { ProfileFoundModal } from "../components/Modals/ProfileFoundModal";
import { ProfileModal } from "../components/Modals/ProfileModal";
import { StatusModal } from "../components/Modals/StatusModal";
import { UserCreationModal } from "../components/Modals/UserCreationModal";
import { UserCreationStartModal } from "../components/Modals/UserCreationStartModal";
import { WalletConnectModal } from "../components/Modals/WalletConnectModal";
import { WalletModal } from "../components/Modals/WalletModal";
import { ToastMessageList } from "../components/ToastMessages/ToastMessageList";
import { ChatState, useChatState } from "../contexts/chatStateProvider";
import { useNarrow } from "../contexts/narrowProvider";
import { GroupChatBody } from "./GroupChat/GroupChatBody";
import { GroupMembers } from "./GroupMembers/GroupMembers";
function Modals() {
return (
<>
<UserCreationModal />
<EditModal />
<ProfileModal />
<StatusModal />
<WalletModal />
<WalletConnectModal />
<CoinbaseModal />
<LogoutModal />
<AgreementModal />
<ProfileFoundModal />
<UserCreationStartModal />
<LeavingModal />
</>
);
}
export function GroupChat() {
const [state] = useChatState();
const [showMembers, setShowMembers] = useState(false);
const [editGroup, setEditGroup] = useState(false);
const narrow = useNarrow();
return (
<ChatWrapper>
{!narrow && (
<ChannelsWrapper>
<Channels setEditGroup={setEditGroup} />
</ChannelsWrapper>
)}
{state === ChatState.ChatBody && (
<GroupChatBody
onClick={() => setShowMembers(!showMembers)}
showMembers={showMembers}
permission={true}
editGroup={editGroup}
setEditGroup={setEditGroup}
/>
)}
{showMembers && !narrow && state === ChatState.ChatBody && (
<GroupMembers />
)}
{state === ChatState.ChatCreation && <ChatCreation />}
<Modals />
<ToastMessageList />
</ChatWrapper>
);
}
const ChatWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
position: relative;
`;
const ChannelsWrapper = styled.div`
width: 21%;
height: 100%;
min-width: 250px;
background-color: ${({ theme }) => theme.sectionBackgroundColor};
padding: 10px 16px;
display: flex;
flex-direction: column;
`;

View File

@ -0,0 +1,182 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import styled from "styled-components";
import { ChatCreation } from "../../components/Chat/ChatCreation";
import { ChatInput } from "../../components/Chat/ChatInput";
import {
ChatTopbar,
ChatTopbarLoading,
} from "../../components/Chat/ChatTopbar";
import { TokenRequirement } from "../../components/Form/TokenRequirement";
import { MessagesList } from "../../components/Messages/MessagesList";
import { NarrowChannels } from "../../components/NarrowMode/NarrowChannels";
import { NarrowMembers } from "../../components/NarrowMode/NarrowMembers";
import { LoadingSkeleton } from "../../components/Skeleton/LoadingSkeleton";
import { useMessengerContext } from "../../contexts/messengerProvider";
import { useNarrow } from "../../contexts/narrowProvider";
import { Reply } from "../../hooks/useReply";
import { ChannelData } from "../../models/ChannelData";
export enum ChatBodyState {
Chat,
Channels,
Members,
}
function ChatBodyLoading() {
const narrow = useNarrow();
return (
<Wrapper>
<ChatBodyWrapper className={narrow ? "narrow" : ""}>
<ChatTopbarLoading />
<LoadingSkeleton />
<ChatInput reply={undefined} setReply={() => undefined} />
</ChatBodyWrapper>
</Wrapper>
);
}
type ChatBodyContentProps = {
showState: ChatBodyState;
switchShowState: (state: ChatBodyState) => void;
channel: ChannelData;
};
function ChatBodyContent({
showState,
switchShowState,
channel,
}: ChatBodyContentProps) {
const [reply, setReply] = useState<Reply | undefined>(undefined);
switch (showState) {
case ChatBodyState.Chat:
return (
<>
<MessagesList setReply={setReply} channel={channel} />
<ChatInput reply={reply} setReply={setReply} />
</>
);
case ChatBodyState.Channels:
return (
<NarrowChannels
setShowChannels={() => switchShowState(ChatBodyState.Channels)}
/>
);
case ChatBodyState.Members:
return (
<NarrowMembers
switchShowMembersList={() => switchShowState(ChatBodyState.Members)}
/>
);
}
}
interface GroupChatBodyProps {
onClick: () => void;
showMembers: boolean;
permission: boolean;
editGroup: boolean;
setEditGroup: React.Dispatch<React.SetStateAction<boolean>>;
}
export function GroupChatBody({
onClick,
showMembers,
permission,
editGroup,
setEditGroup,
}: GroupChatBodyProps) {
const { activeChannel, loadingMessenger } = useMessengerContext();
const narrow = useNarrow();
const className = useMemo(() => (narrow ? "narrow" : ""), [narrow]);
const [showState, setShowState] = useState<ChatBodyState>(ChatBodyState.Chat);
const switchShowState = useCallback(
(state: ChatBodyState) => {
if (narrow) {
setShowState((prev) => (prev === state ? ChatBodyState.Chat : state));
}
},
[narrow]
);
useEffect(() => {
if (!narrow) {
setShowState(ChatBodyState.Chat);
}
}, [narrow]);
if (!loadingMessenger && activeChannel) {
return (
<Wrapper>
<ChatBodyWrapper className={className}>
{editGroup ? (
<ChatCreation
setEditGroup={setEditGroup}
activeChannel={activeChannel}
/>
) : (
<ChatTopbar
onClick={onClick}
setEditGroup={setEditGroup}
showMembers={showMembers}
showState={showState}
switchShowState={switchShowState}
/>
)}
<ChatBodyContent
showState={showState}
switchShowState={switchShowState}
channel={activeChannel}
/>
</ChatBodyWrapper>
{!permission && (
<BluredWrapper>
<TokenRequirement />
</BluredWrapper>
)}
</Wrapper>
);
}
return <ChatBodyLoading />;
}
export const Wrapper = styled.div`
width: 61%;
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
background: ${({ theme }) => theme.bodyBackgroundColor};
position: relative;
&.narrow {
width: 100%;
}
`;
const ChatBodyWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
flex: 1;
background: ${({ theme }) => theme.bodyBackgroundColor};
`;
const BluredWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: flex-end;
justify-content: center;
position: absolute;
bottom: 0;
left: 0;
background: ${({ theme }) => theme.bodyBackgroundGradient};
backdrop-filter: blur(4px);
z-index: 2;
`;

View File

@ -0,0 +1,49 @@
import React, { useMemo, useState } from "react";
import styled from "styled-components";
import { MembersList } from "../../components/Members/MembersList";
import { useMessengerContext } from "../../contexts/messengerProvider";
export function GroupMembers() {
const { addContact, activeChannel } = useMessengerContext();
const heading = useMemo(
() =>
activeChannel && activeChannel?.type === "group"
? "Group members"
: "Members",
[activeChannel]
);
const [newUserInput, setNewUserInput] = useState("");
return (
<>
<MembersWrapper>
<MemberHeading>{heading}</MemberHeading>
<MembersList />
<input
value={newUserInput}
onChange={(e) => setNewUserInput(e.target.value)}
/>
<button onClick={() => addContact(newUserInput)}>Add Contact</button>
</MembersWrapper>
</>
);
}
const MembersWrapper = styled.div`
width: 18%;
height: 100%;
min-width: 164px;
display: flex;
flex-direction: column;
background-color: ${({ theme }) => theme.sectionBackgroundColor};
padding: 16px;
overflow-y: scroll;
`;
const MemberHeading = styled.h2`
font-weight: 500;
font-size: 15px;
line-height: 22px;
color: ${({ theme }) => theme.primary};
margin-bottom: 16px;
`;

View File

@ -1,4 +1,5 @@
import {
Contacts as ContactsClass,
GroupChat,
GroupChats,
Identity,
@ -28,10 +29,11 @@ export function useGroupChats(
messenger: Messenger | undefined,
identity: Identity | undefined,
dispatch: (action: ChannelAction) => void,
addChatMessage: (newMessage: ChatMessage | undefined, id: string) => void
addChatMessage: (newMessage: ChatMessage | undefined, id: string) => void,
contactsClass: ContactsClass | undefined
) {
const groupChat = useMemo(() => {
if (messenger && identity) {
if (messenger && identity && contactsClass) {
const addChat = (chat: GroupChat) => {
const members = chat.members
.map((member) => member.id)
@ -52,6 +54,7 @@ export function useGroupChats(
description: `Chatkey: ${chat.members[0].id}`,
members,
};
chat.members.forEach((member) => contactsClass.addContact(member.id));
dispatch({ type: "AddChannel", payload: channel });
};
const removeChat = (chat: GroupChat) => {
@ -81,7 +84,7 @@ export function useGroupChats(
handleMessage
);
}
}, [messenger, identity]);
}, [messenger, identity, contactsClass]);
const createGroupChat = useCallback(
(members: string[]) => {

View File

@ -17,12 +17,14 @@ export function useLoadPrevDay(
const [loadingMessages, setLoadingMessages] = useState(false);
useEffect(() => {
if (chatId) {
setLoadingMessages(loadingPreviousMessages.current[chatId]);
}
}, [chatId]);
const loadPrevDay = useCallback(
async (id: string, groupChat?: boolean) => {
if (messenger) {
if (messenger && id) {
const endTime = lastLoadTime.current[id] ?? new Date();
const startTime = new Date(endTime.getTime() - _MS_PER_DAY * 5);
const timeDiff = Math.floor(

View File

@ -41,6 +41,7 @@ export type MessengerType = {
communityData: CommunityData | undefined;
contacts: Contacts;
contactsDispatch: (action: ContactsAction) => void;
addContact: (publicKey: string) => void;
channels: ChannelsData;
channelsDispatch: (action: ChannelAction) => void;
removeChannel: (channelId: string) => void;
@ -105,7 +106,7 @@ function useCreateCommunity(
}
export function useMessenger(
communityKey: string,
communityKey: string | undefined,
identity: Identity | undefined,
newNickname: string | undefined
) {
@ -117,6 +118,15 @@ export function useMessenger(
newNickname
);
const addContact = useCallback(
(publicKey: string) => {
if (contactsClass) {
contactsClass.addContact(publicKey);
}
},
[contactsClass]
);
const {
addChatMessage,
addMessage,
@ -176,7 +186,13 @@ export function useMessenger(
createGroupChat,
changeGroupChatName,
addMembers,
} = useGroupChats(messenger, identity, channelsDispatch, addChatMessage);
} = useGroupChats(
messenger,
identity,
channelsDispatch,
addChatMessage,
contactsClass
);
const { loadPrevDay, loadingMessages } = useLoadPrevDay(
channelsState.activeChannel.id,
@ -237,9 +253,12 @@ export function useMessenger(
}, [notifications, channelsState]);
const loadingMessenger = useMemo(() => {
return !communityData || !messenger || !channelsState.activeChannel.id;
return Boolean(
(communityKey && !communityData) ||
!messenger ||
(communityKey && !channelsState.activeChannel.id)
);
}, [communityData, messenger, channelsState]);
return {
messenger,
messages,
@ -252,6 +271,7 @@ export function useMessenger(
communityData,
contacts,
contactsDispatch,
addContact,
channels: channelsState.channels,
channelsDispatch,
removeChannel,

View File

@ -22,16 +22,16 @@ export function useChatScrollHandle(
(ref?.current?.clientHeight ?? 0) >= (ref?.current?.scrollHeight ?? 0)
) {
setScrollOnBot(true);
loadPrevDay(activeChannel.id, activeChannel.type === "group");
loadPrevDay(activeChannel.id, activeChannel.type !== "channel");
}
}
}, [messages.length, loadingMessages, activeChannel]);
}, [messages.length, activeChannel]);
useEffect(() => {
const setScroll = () => {
if (ref?.current && activeChannel) {
if (ref.current.scrollTop <= 0) {
loadPrevDay(activeChannel.id, activeChannel.type === "group");
loadPrevDay(activeChannel.id, activeChannel.type !== "channel");
}
if (
ref.current.scrollTop + ref.current.clientHeight ==

View File

@ -1,4 +1,12 @@
import { DappConnectCommunityChat } from "./components/DappConnectCommunityChat";
import { ConfigType } from "./contexts/configProvider";
import { DappConnectGroupChat } from "./groupChatComponents/DappConnectGroupChat";
import { darkTheme, lightTheme } from "./styles/themes";
export { DappConnectCommunityChat, lightTheme, darkTheme, ConfigType };
export {
DappConnectCommunityChat,
DappConnectGroupChat,
lightTheme,
darkTheme,
ConfigType,
};

View File

@ -0,0 +1,41 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": { "project": "./tsconfig.json" },
"env": { "es6": true },
"ignorePatterns": ["node_modules", "dist", "coverage", "proto"],
"plugins": ["import", "eslint-comments", "functional"],
"extends": [
"eslint:recommended",
"plugin:eslint-comments/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/typescript",
"prettier"
],
"globals": { "BigInt": true, "console": true, "WebAssembly": true },
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off",
"eslint-comments/disable-enable-pair": [
"error",
{ "allowWholeFile": true }
],
"eslint-comments/no-unused-disable": "error",
"import/order": [
"error",
{ "newlines-between": "always", "alphabetize": { "order": "asc" } }
],
"no-constant-condition": ["error", { "checkLoops": false }],
"sort-imports": [
"error",
{ "ignoreDeclarationSort": true, "ignoreCase": true }
]
},
"overrides": [
{
"files": ["*.spec.ts", "**/test_utils/*.ts"],
"rules": {
"@typescript-eslint/no-non-null-assertion": "off"
}
}
]
}

View File

@ -0,0 +1,6 @@
{
"extension": ["ts"],
"spec": "src/**/*.spec.ts",
"require": "ts-node/register",
"exit": true
}

View File

@ -0,0 +1,2 @@
# package.json is formatted by package managers, so we ignore it here
package.json

View File

@ -0,0 +1 @@
#React chat example

View File

@ -0,0 +1,71 @@
{
"name": "@waku/react-group-chat-sdk-example",
"main": "index.js",
"version": "0.1.0",
"repository": "https://github.com/status-im/dappconnect-chat-sdk/",
"license": "MIT OR Apache-2.0",
"packageManager": "yarn@3.0.1",
"scripts": {
"clean:all": "yarn clean && rimraf node_modules/",
"clean": "rimraf dist/",
"build": "rm -rf dist && webpack --mode=production --env ENV=production",
"start": "webpack serve --mode=development --env ENV=$ENV COMMUNITY_KEY=$COMMUNITY_KEY --https",
"fix": "run-s 'fix:*'",
"fix:prettier": "prettier './{src,test}/**/*.{ts,tsx}' \"./*.json\" --write",
"fix:lint": "eslint './{src,test}/**/*.{ts,tsx}' --fix",
"test": "run-s 'test:*'",
"test:lint": "eslint './{src,test}/**/*.{ts,tsx}'",
"test:prettier": "prettier './{src,test}/**/*.{ts,tsx}' \"./*.json\" --list-different"
},
"dependencies": {
"@waku/react-chat-sdk": "^0.1.0",
"assert": "^2.0.0",
"browserify-zlib": "^0.2.0",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.0",
"https-browserify": "^1.0.0",
"process": "^0.11.10",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.2.0",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"styled-components": "^5.3.1"
},
"devDependencies": {
"@testing-library/react-hooks": "^7.0.1",
"@types/chai": "^4.2.21",
"@types/mocha": "^9.0.0",
"@types/node": "^16.4.12",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"@types/react-router": "^5.1.16",
"@types/react-router-dom": "^5.1.8",
"@types/styled-components": "^5.1.12",
"@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0",
"chai": "^4.3.4",
"css-loader": "^6.3.0",
"esbuild-loader": "^2.15.1",
"eslint": "^7.32.0",
"eslint-plugin-hooks": "^0.2.0",
"eslint-plugin-react": "^7.24.0",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^6.3.1",
"html-webpack-plugin": "^5.3.2",
"jsdom": "^16.7.0",
"jsdom-global": "^3.0.2",
"mocha": "^9.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"source-map-loader": "^3.0.0",
"style-loader": "^3.3.0",
"ts-loader": "^9.2.5",
"ts-node": "^10.1.0",
"typescript": "^4.3.5",
"webpack": "^5.48.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2"
}
}

View File

@ -0,0 +1 @@
/* /index.html 200

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0,minimum-scale=1" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<title>DAppconnect group chat</title>
</head>
<body>
<div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="/index.js"></script>
</body>
</html>

View File

@ -0,0 +1,139 @@
import {
DappConnectGroupChat,
darkTheme,
lightTheme,
} from "@waku/react-chat-sdk";
import React, { useRef, useState } from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";
const fetchMetadata = async (link: string) => {
const response = await fetch("https://localhost:3000", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ site: link }),
});
const body = await response.text();
const parsedBody = JSON.parse(body);
if (
"og:image" in parsedBody &&
"og:site_name" in parsedBody &&
"og:title" in parsedBody
) {
return JSON.parse(body);
}
};
function DragDiv() {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
const [width, setWidth] = useState(window.innerWidth - 50);
const [height, setHeight] = useState(window.innerHeight - 50);
const [showChat, setShowChat] = useState(true);
const ref = useRef<HTMLHeadingElement>(null);
const moved = useRef(false);
const setting = useRef("");
const [theme, setTheme] = useState(true);
const onMouseMove = (e: MouseEvent) => {
if (setting.current === "position") {
e.preventDefault();
setX(e.x - 20);
setY(e.y - 20);
}
if (setting.current === "size") {
setWidth(e.x - x);
setHeight(e.y - y);
e.preventDefault();
}
moved.current = true;
};
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
if (!moved.current) [setShowChat((prev) => !prev)];
moved.current = false;
};
return (
<>
<button
onClick={() => {
setTheme(!theme);
}}
>
Change theme
</button>
<Drag style={{ left: x, top: y, width: width, height: height }} ref={ref}>
<Bubble
onMouseDown={() => {
setting.current = "position";
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
}}
/>
<FloatingDiv className={showChat ? "" : "hide"}>
<DappConnectGroupChat
theme={theme ? lightTheme : darkTheme}
config={{
environment: process.env.ENV ?? "",
dappUrl: "https://0.0.0.0:8080",
}}
fetchMetadata={fetchMetadata}
/>
</FloatingDiv>
{showChat && (
<SizeSet
onMouseDown={() => {
setting.current = "size";
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
}}
></SizeSet>
)}
</Drag>
</>
);
}
const FloatingDiv = styled.div`
height: calc(100% - 50px);
border: 1px solid black;
&.hide {
display: none;
}
`;
const SizeSet = styled.div`
margin-left: auto;
margin-right: 0px;
width: 10px;
height: 10px;
background-color: light-grey;
border: 1px solid;
`;
const Bubble = styled.div`
width: 50px;
height: 50px;
border-radius: 50%;
background-color: lightblue;
border: 1px solid;
`;
const Drag = styled.div`
position: absolute;
min-width: 375px;
`;
ReactDOM.render(
<div style={{ height: "100%" }}>
<DragDiv />
</div>,
document.getElementById("root")
);

View File

@ -0,0 +1,47 @@
{
"compilerOptions": {
"target": "es6",
"outDir": "dist",
"jsx": "react",
"moduleResolution": "node",
"module": "commonjs",
"declaration": true,
"sourceMap": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"composite": true,
"strict": true /* Enable all strict type-checking options. */,
/* Strict Type-Checking Options */
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
/* Additional Checks */
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": false /* to set at a later stage */,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
/* Debugging Options */
"traceResolution": false,
"listEmittedFiles": false,
"listFiles": false,
"pretty": true,
// Due to broken types in indirect dependencies
"skipLibCheck": true,
"typeRoots": [
"./node_modules/@types",
"./src/types",
"../../node_modules/@types"
]
},
"include": ["src"],
"types": ["mocha"],
"compileOnSave": false
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,88 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const webpack = require('webpack');
const { ESBuildMinifyPlugin } = require('esbuild-loader');
module.exports = env => {
const environment = env.ENV || 'development';
const communityKey = env.COMMUNITY_KEY || '';
return {
entry: './src/index.tsx',
output: {
filename: 'index.[fullhash].js',
path: path.join(__dirname, 'dist'),
publicPath: '/',
},
devtool: 'source-map',
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
fallback: {
buffer: require.resolve('buffer/'),
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
assert: require.resolve('assert/'),
http: require.resolve('stream-http'),
https: require.resolve('https-browserify'),
zlib: require.resolve('browserify-zlib')
},
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'esbuild-loader',
exclude: /node_modules/,
options: {
loader: 'tsx',
target: 'es2018',
},
},
{
enforce: 'pre',
test: /\.js$/,
exclude: /node_modules/,
loader: 'source-map-loader',
},
{
test: /\.(png|svg|jpg|gif|woff|woff2|eot|ttf|otf|ico)$/,
use: ['file-loader'],
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
optimization: {
minimizer: [
new ESBuildMinifyPlugin({
target: 'es2018',
}),
],
},
plugins: [
new ForkTsCheckerWebpackPlugin(),
new HtmlWebpackPlugin({
template: 'src/index.html',
}),
new webpack.DefinePlugin({
'process.env.ENV': JSON.stringify(environment),
'process.env.COMMUNITY_KEY': JSON.stringify(communityKey),
}),
new webpack.ProvidePlugin({
process: 'process/browser.js',
Buffer: ['buffer', 'Buffer'],
}),
],
devServer: {
historyApiFallback: true,
host: '0.0.0.0',
stats: 'errors-only',
overlay: true,
hot: true,
},
stats: 'minimal',
};
};

View File

@ -1343,6 +1343,60 @@ __metadata:
languageName: unknown
linkType: soft
"@waku/react-group-chat-sdk-example@workspace:packages/react-group-chat-example":
version: 0.0.0-use.local
resolution: "@waku/react-group-chat-sdk-example@workspace:packages/react-group-chat-example"
dependencies:
"@testing-library/react-hooks": ^7.0.1
"@types/chai": ^4.2.21
"@types/mocha": ^9.0.0
"@types/node": ^16.4.12
"@types/react": ^17.0.15
"@types/react-dom": ^17.0.9
"@types/react-router": ^5.1.16
"@types/react-router-dom": ^5.1.8
"@types/styled-components": ^5.1.12
"@typescript-eslint/eslint-plugin": ^4.29.0
"@typescript-eslint/parser": ^4.29.0
"@waku/react-chat-sdk": ^0.1.0
assert: ^2.0.0
browserify-zlib: ^0.2.0
buffer: ^6.0.3
chai: ^4.3.4
crypto-browserify: ^3.12.0
css-loader: ^6.3.0
esbuild-loader: ^2.15.1
eslint: ^7.32.0
eslint-plugin-hooks: ^0.2.0
eslint-plugin-react: ^7.24.0
file-loader: ^6.2.0
fork-ts-checker-webpack-plugin: ^6.3.1
html-webpack-plugin: ^5.3.2
https-browserify: ^1.0.0
jsdom: ^16.7.0
jsdom-global: ^3.0.2
mocha: ^9.0.3
npm-run-all: ^4.1.5
prettier: ^2.3.2
process: ^0.11.10
react: ^17.0.2
react-dom: ^17.0.2
react-router-dom: ^5.2.0
rimraf: ^3.0.2
source-map-loader: ^3.0.0
stream-browserify: ^3.0.0
stream-http: ^3.2.0
style-loader: ^3.3.0
styled-components: ^5.3.1
ts-loader: ^9.2.5
ts-node: ^10.1.0
typescript: ^4.3.5
webpack: ^5.48.0
webpack-cli: ^4.7.2
webpack-dev-server: ^3.11.2
languageName: unknown
linkType: soft
"@waku/status-communities@^0.1.0, @waku/status-communities@workspace:packages/status-communities":
version: 0.0.0-use.local
resolution: "@waku/status-communities@workspace:packages/status-communities"