Introduce online status broadcast (#99)

This commit is contained in:
Szymon Szlachtowicz 2021-10-28 09:47:14 +02:00 committed by GitHub
parent f957aa76cf
commit 7c3f256e61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 610 additions and 168 deletions

View File

@ -1,4 +1,4 @@
import { lightTheme, ReactChat } from "@dappconnect/react-chat"; import { darkTheme, lightTheme, ReactChat } from "@dappconnect/react-chat";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import styled from "styled-components"; import styled from "styled-components";
@ -32,6 +32,8 @@ function DragDiv() {
const moved = useRef(false); const moved = useRef(false);
const setting = useRef(""); const setting = useRef("");
const [theme, setTheme] = useState(true);
const onMouseMove = (e: MouseEvent) => { const onMouseMove = (e: MouseEvent) => {
if (setting.current === "position") { if (setting.current === "position") {
e.preventDefault(); e.preventDefault();
@ -54,33 +56,42 @@ function DragDiv() {
}; };
return ( return (
<Drag style={{ left: x, top: y, width: width, height: height }} ref={ref}> <>
<Bubble <button
onMouseDown={() => { onClick={() => {
setting.current = "position"; setTheme(!theme);
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
}} }}
/> >
<FloatingDiv className={showChat ? "" : "hide"}> Change theme
<ReactChat </button>
theme={lightTheme} <Drag style={{ left: x, top: y, width: width, height: height }} ref={ref}>
communityKey={ <Bubble
"0x0262c65c881f5a9f79343a26faaa02aad3af7c533d9445fb1939ed11b8bf4d2abd"
}
fetchMetadata={fetchMetadata}
/>
</FloatingDiv>
{showChat && (
<SizeSet
onMouseDown={() => { onMouseDown={() => {
setting.current = "size"; setting.current = "position";
document.addEventListener("mousemove", onMouseMove); document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp); document.addEventListener("mouseup", onMouseUp);
}} }}
></SizeSet> />
)} <FloatingDiv className={showChat ? "" : "hide"}>
</Drag> <ReactChat
theme={theme ? lightTheme : darkTheme}
communityKey={
"0x0262c65c881f5a9f79343a26faaa02aad3af7c533d9445fb1939ed11b8bf4d2abd"
}
fetchMetadata={fetchMetadata}
/>
</FloatingDiv>
{showChat && (
<SizeSet
onMouseDown={() => {
setting.current = "size";
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
}}
></SizeSet>
)}
</Drag>
</>
); );
} }

View File

@ -49,6 +49,7 @@ export function Chat({
loadPrevDay, loadPrevDay,
loadingMessages, loadingMessages,
community, community,
contacts,
} = useMessenger(activeChannel?.id ?? "", communityKey, identity); } = useMessenger(activeChannel?.id ?? "", communityKey, identity);
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
@ -68,14 +69,7 @@ export function Chat({
description: community.description.identity?.description ?? "", description: community.description.identity?.description ?? "",
}; };
} else { } else {
return { return undefined;
id: 1,
name: "",
icon: "",
members: 0,
membersList: [],
description: "",
};
} }
}, [community]); }, [community]);
@ -101,7 +95,7 @@ export function Chat({
<ChatWrapper> <ChatWrapper>
{showChannels && !narrow && ( {showChannels && !narrow && (
<ChannelsWrapper> <ChannelsWrapper>
{messenger ? ( {community && communityData ? (
<StyledCommunity onClick={showModal} community={communityData} /> <StyledCommunity onClick={showModal} community={communityData} />
) : ( ) : (
<CommunitySkeleton /> <CommunitySkeleton />
@ -117,6 +111,8 @@ export function Chat({
</ChannelsWrapper> </ChannelsWrapper>
)} )}
<ChatBody <ChatBody
identity={identity}
contacts={contacts}
theme={theme} theme={theme}
channel={activeChannel} channel={activeChannel}
messenger={messenger} messenger={messenger}
@ -140,20 +136,23 @@ export function Chat({
/> />
{showMembers && !narrow && ( {showMembers && !narrow && (
<Members <Members
community={communityData} identity={identity}
contacts={contacts}
setShowChannels={setShowChannels} setShowChannels={setShowChannels}
setMembersList={setMembersList} setMembersList={setMembersList}
/> />
)} )}
<CommunityModal {communityData && (
isVisible={isModalVisible} <CommunityModal
onClose={() => setIsModalVisible(false)} isVisible={isModalVisible}
icon={communityData.icon} onClose={() => setIsModalVisible(false)}
name={communityData.name} icon={communityData.icon}
subtitle="Public Community" name={communityData.name}
description={communityData.description} subtitle="Public Community"
publicKey={communityKey} description={communityData.description}
/> publicKey={communityKey}
/>
)}
</ChatWrapper> </ChatWrapper>
); );
} }

View File

@ -1,10 +1,12 @@
import React, { useCallback, useEffect, useMemo, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Identity } from "status-communities/dist/cjs";
import styled from "styled-components"; import styled from "styled-components";
import { useNarrow } from "../../contexts/narrowProvider"; import { useNarrow } from "../../contexts/narrowProvider";
import { ChannelData } from "../../models/ChannelData"; import { ChannelData } from "../../models/ChannelData";
import { ChatMessage } from "../../models/ChatMessage"; import { ChatMessage } from "../../models/ChatMessage";
import { CommunityData } from "../../models/CommunityData"; import { CommunityData } from "../../models/CommunityData";
import { Contact } from "../../models/Contact";
import { Metadata } from "../../models/Metadata"; import { Metadata } from "../../models/Metadata";
import { Theme } from "../../styles/themes"; import { Theme } from "../../styles/themes";
import { Channel } from "../Channels/Channel"; import { Channel } from "../Channels/Channel";
@ -21,9 +23,11 @@ import { ChatInput } from "./ChatInput";
import { ChatMessages } from "./ChatMessages"; import { ChatMessages } from "./ChatMessages";
interface ChatBodyProps { interface ChatBodyProps {
identity: Identity;
contacts: Contact[];
theme: Theme; theme: Theme;
channel: ChannelData; channel: ChannelData;
community: CommunityData; community: CommunityData | undefined;
messenger: any; messenger: any;
messages: ChatMessage[]; messages: ChatMessage[];
sendMessage: (text: string, image?: Uint8Array) => void; sendMessage: (text: string, image?: Uint8Array) => void;
@ -44,6 +48,8 @@ interface ChatBodyProps {
} }
export function ChatBody({ export function ChatBody({
identity,
contacts,
theme, theme,
channel, channel,
community, community,
@ -95,7 +101,7 @@ export function ChatBody({
} }
> >
<ChannelWrapper className={className}> <ChannelWrapper className={className}>
{messenger ? ( {messenger && community ? (
<> <>
{(showCommunity || narrow) && ( {(showCommunity || narrow) && (
<CommunityWrap className={className}> <CommunityWrap className={className}>
@ -125,14 +131,14 @@ export function ChatBody({
> >
<MembersIcon /> <MembersIcon />
</MemberBtn> </MemberBtn>
{!messenger && <Loading />} {!community && <Loading />}
</ChatTopbar> </ChatTopbar>
{messenger ? ( {messenger && community ? (
<> <>
{!showChannelsList && !showMembersList && ( {!showChannelsList && !showMembersList && (
<> <>
{messages.length > 0 ? ( {messages.length > 0 ? (
messenger ? ( messenger && community ? (
<ChatMessages <ChatMessages
messages={messages} messages={messages}
loadPrevDay={loadPrevDay} loadPrevDay={loadPrevDay}
@ -163,6 +169,8 @@ export function ChatBody({
)} )}
{showMembersList && narrow && ( {showMembersList && narrow && (
<NarrowMembers <NarrowMembers
identity={identity}
contacts={contacts}
community={community} community={community}
setShowChannels={setShowChannelsList} setShowChannels={setShowChannelsList}
setShowMembersList={setShowMembersList} setShowMembersList={setShowMembersList}

View File

@ -23,6 +23,7 @@ export function ChatInput({ theme, addMessage }: ChatInputProps) {
const [inputHeight, setInputHeight] = useState(40); const [inputHeight, setInputHeight] = useState(40);
const [imageUint, setImageUint] = useState<undefined | Uint8Array>(undefined); const [imageUint, setImageUint] = useState<undefined | Uint8Array>(undefined);
const [showSizeLimit, setShowSizeLimit] = useState(false); const [showSizeLimit, setShowSizeLimit] = useState(false);
useEffect(() => { useEffect(() => {
window.addEventListener("click", () => setShowEmoji(false)); window.addEventListener("click", () => setShowEmoji(false));
return () => { return () => {
@ -127,18 +128,17 @@ export function ChatInput({ theme, addMessage }: ChatInputProps) {
/> />
</AddPictureInputWrapper> </AddPictureInputWrapper>
<Row style={{ height: `${inputHeight + (image ? 73 : 0)}px` }}> <Row style={{ height: `${inputHeight + (image ? 73 : 0)}px` }}>
{image && ( <InputWrapper>
<ImagePreviewWrapper> {image && (
<ImagePreviewOverlay />
<ImagePreview src={image} onClick={() => setImageUint(undefined)} /> <ImagePreview src={image} onClick={() => setImageUint(undefined)} />
</ImagePreviewWrapper> )}
)} <Input
<Input placeholder="Message"
placeholder="Message" value={content}
value={content} onChange={onInputChange}
onChange={onInputChange} onKeyPress={onInputKeyPress}
onKeyPress={onInputKeyPress} />
/> </InputWrapper>
<InputButtons> <InputButtons>
<ChatButton <ChatButton
onClick={(e) => { onClick={(e) => {
@ -160,6 +160,12 @@ export function ChatInput({ theme, addMessage }: ChatInputProps) {
); );
} }
const InputWrapper = styled.div`
display: flex;
flex-direction: column;
width: 100%;
`;
const View = styled.div` const View = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
@ -187,28 +193,7 @@ const InputButtons = styled.div`
} }
`; `;
const ImagePreviewWrapper = styled.div`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 82px;
z-index: 1;
`;
const ImagePreviewOverlay = styled.div`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #eef2f5;
border-radius: 16px 16px 4px 16px;
opacity: 0.9;
`;
const ImagePreview = styled.img` const ImagePreview = styled.img`
position: relative;
width: 64px; width: 64px;
height: 64px; height: 64px;
border-radius: 16px 16px 4px 16px; border-radius: 16px 16px 4px 16px;

View File

@ -1,18 +1,21 @@
import React from "react"; import React from "react";
import { Identity } from "status-communities/dist/cjs";
import styled from "styled-components"; import styled from "styled-components";
import { CommunityData } from "../../models/CommunityData"; import { Contact } from "../../models/Contact";
import { MembersList } from "./MembersList"; import { MembersList } from "./MembersList";
interface MembersProps { interface MembersProps {
community: CommunityData; identity: Identity;
contacts: Contact[];
setShowChannels: (val: boolean) => void; setShowChannels: (val: boolean) => void;
setMembersList: any; setMembersList: any;
} }
export function Members({ export function Members({
community, identity,
contacts,
setShowChannels, setShowChannels,
setMembersList, setMembersList,
}: MembersProps) { }: MembersProps) {
@ -20,7 +23,8 @@ export function Members({
<MembersWrapper> <MembersWrapper>
<MemberHeading>Members</MemberHeading> <MemberHeading>Members</MemberHeading>
<MembersList <MembersList
community={community} identity={identity}
contacts={contacts}
setShowChannels={setShowChannels} setShowChannels={setShowChannels}
setMembersList={setMembersList} setMembersList={setMembersList}
/> />

View File

@ -1,20 +1,24 @@
import React from "react"; import React from "react";
import { Identity, utils } from "status-communities/dist/cjs";
import { bufToHex } from "status-communities/dist/cjs/utils";
import styled from "styled-components"; import styled from "styled-components";
import { CommunityData } from "../../models/CommunityData"; import { Contact } from "../../models/Contact";
import { UserIcon } from "../Icons/UserIcon"; import { UserIcon } from "../Icons/UserIcon";
import { Member, MemberData, MemberIcon } from "./Member"; import { Member, MemberData, MemberIcon } from "./Member";
interface MembersListProps { interface MembersListProps {
community: CommunityData; identity: Identity;
contacts: Contact[];
setShowChannels: (val: boolean) => void; setShowChannels: (val: boolean) => void;
setShowMembers?: (val: boolean) => void; setShowMembers?: (val: boolean) => void;
setMembersList: any; setMembersList: any;
} }
export function MembersList({ export function MembersList({
community, identity,
contacts,
setShowChannels, setShowChannels,
setShowMembers, setShowMembers,
setMembersList, setMembersList,
@ -27,18 +31,19 @@ export function MembersList({
<MemberIcon> <MemberIcon>
<UserIcon memberView={true} /> <UserIcon memberView={true} />
</MemberIcon> </MemberIcon>
<MemberName>Guest564732</MemberName> <MemberName>{utils.bufToHex(identity.publicKey)}</MemberName>
</MemberData> </MemberData>
</MemberCategory> </MemberCategory>
<MemberCategory> <MemberCategory>
<MemberCategoryName>Online</MemberCategoryName> <MemberCategoryName>Online</MemberCategoryName>
{community.membersList {contacts
.filter(() => false) .filter((e) => e.id != bufToHex(identity.publicKey))
.map((member) => ( .filter((e) => e.online)
.map((contact) => (
<Member <Member
key={member} key={contact.id}
member={member} member={contact.id}
isOnline={false} isOnline={contact.online}
setShowChannels={setShowChannels} setShowChannels={setShowChannels}
setShowMembers={setShowMembers} setShowMembers={setShowMembers}
setMembersList={setMembersList} setMembersList={setMembersList}
@ -47,16 +52,19 @@ export function MembersList({
</MemberCategory> </MemberCategory>
<MemberCategory> <MemberCategory>
<MemberCategoryName>Offline</MemberCategoryName> <MemberCategoryName>Offline</MemberCategoryName>
{community.membersList.map((member) => ( {contacts
<Member .filter((e) => e.id != bufToHex(identity.publicKey))
key={member} .filter((e) => !e.online)
member={member} .map((contact) => (
isOnline={false} <Member
setShowChannels={setShowChannels} key={contact.id}
setShowMembers={setShowMembers} member={contact.id}
setMembersList={setMembersList} isOnline={contact.online}
/> setShowChannels={setShowChannels}
))} setShowMembers={setShowMembers}
setMembersList={setMembersList}
/>
))}
</MemberCategory> </MemberCategory>
</MembersListWrap> </MembersListWrap>
); );

View File

@ -1,20 +1,26 @@
import React from "react"; import React from "react";
import { Identity } from "status-communities/dist/cjs";
import styled from "styled-components"; import styled from "styled-components";
import { CommunityData } from "../../models/CommunityData"; import { CommunityData } from "../../models/CommunityData";
import { Contact } from "../../models/Contact";
import { MembersList } from "../Members/MembersList"; import { MembersList } from "../Members/MembersList";
import { NarrowTopbar } from "./NarrowTopbar"; import { NarrowTopbar } from "./NarrowTopbar";
interface NarrowMembersProps { interface NarrowMembersProps {
identity: Identity;
community: CommunityData; community: CommunityData;
contacts: Contact[];
setShowChannels: (val: boolean) => void; setShowChannels: (val: boolean) => void;
setShowMembersList: (val: boolean) => void; setShowMembersList: (val: boolean) => void;
setMembersList: any; setMembersList: any;
} }
export function NarrowMembers({ export function NarrowMembers({
identity,
community, community,
contacts,
setShowChannels, setShowChannels,
setShowMembersList, setShowMembersList,
setMembersList, setMembersList,
@ -23,7 +29,8 @@ export function NarrowMembers({
<ListWrapper> <ListWrapper>
<NarrowTopbar list="Community members" community={community.name} /> <NarrowTopbar list="Community members" community={community.name} />
<MembersList <MembersList
community={community} identity={identity}
contacts={contacts}
setShowChannels={setShowChannels} setShowChannels={setShowChannels}
setShowMembers={setShowMembersList} setShowMembers={setShowMembersList}
setMembersList={setMembersList} setMembersList={setMembersList}

View File

@ -1,12 +1,15 @@
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { ApplicationMetadataMessage } from "status-communities/dist/cjs"; import {
ApplicationMetadataMessage,
Contacts,
} from "status-communities/dist/cjs";
import { ChatMessage } from "../../models/ChatMessage"; import { ChatMessage } from "../../models/ChatMessage";
import { binarySetInsert } from "../../utils"; import { binarySetInsert } from "../../utils";
import { useNotifications } from "./useNotifications"; import { useNotifications } from "./useNotifications";
export function useMessages(chatId: string) { export function useMessages(chatId: string, contacts?: Contacts) {
const [messages, setMessages] = useState<{ [chatId: string]: ChatMessage[] }>( const [messages, setMessages] = useState<{ [chatId: string]: ChatMessage[] }>(
{} {}
); );
@ -17,6 +20,9 @@ export function useMessages(chatId: string) {
(msg: ApplicationMetadataMessage, id: string, date: Date) => { (msg: ApplicationMetadataMessage, id: string, date: Date) => {
const newMessage = ChatMessage.fromMetadataMessage(msg, date); const newMessage = ChatMessage.fromMetadataMessage(msg, date);
if (newMessage) { if (newMessage) {
if (contacts) {
contacts.addContact(newMessage.sender);
}
setMessages((prev) => { setMessages((prev) => {
return { return {
...prev, ...prev,
@ -31,7 +37,7 @@ export function useMessages(chatId: string) {
incNotification(id); incNotification(id);
} }
}, },
[] [contacts]
); );
const activeMessages = useMemo( const activeMessages = useMemo(

View File

@ -1,8 +1,16 @@
// import { StoreCodec } from "js-waku"; // import { StoreCodec } from "js-waku";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { Community, Identity, Messenger } from "status-communities/dist/cjs"; import {
Community,
Contacts,
Identity,
Messenger,
utils,
} from "status-communities/dist/cjs";
import { createCommunityMessenger } from "../../utils/createCommunityMessenger"; import { Contact } from "../../models/Contact";
import { createCommunity } from "../../utils/createCommunity";
import { createMessenger } from "../../utils/createMessenger";
import { useLoadPrevDay } from "./useLoadPrevDay"; import { useLoadPrevDay } from "./useLoadPrevDay";
import { useMessages } from "./useMessages"; import { useMessages } from "./useMessages";
@ -13,27 +21,65 @@ export function useMessenger(
identity: Identity identity: Identity
) { ) {
const [messenger, setMessenger] = useState<Messenger | undefined>(undefined); const [messenger, setMessenger] = useState<Messenger | undefined>(undefined);
const [internalContacts, setInternalContacts] = useState<{
[id: string]: number;
}>({});
const contactsClass = useMemo(() => {
if (messenger) {
const newContacts = new Contacts(
identity,
messenger.waku,
(id, clock) => {
setInternalContacts((prev) => {
return { ...prev, [id]: clock };
});
}
);
newContacts.addContact(utils.bufToHex(identity.publicKey));
return newContacts;
}
}, [messenger]);
const contacts = useMemo<Contact[]>(() => {
const now = Date.now();
const newContacts: Contact[] = [];
Object.entries(internalContacts).forEach(([id, clock]) => {
newContacts.push({
id,
online: clock > now - 301000,
});
});
return newContacts;
}, [internalContacts]);
const { addMessage, clearNotifications, notifications, messages } = const { addMessage, clearNotifications, notifications, messages } =
useMessages(chatId); useMessages(chatId, contactsClass);
const [community, setCommunity] = useState<Community | undefined>(undefined); const [community, setCommunity] = useState<Community | undefined>(undefined);
const { loadPrevDay, loadingMessages } = useLoadPrevDay(chatId, messenger); const { loadPrevDay, loadingMessages } = useLoadPrevDay(chatId, messenger);
useEffect(() => { useEffect(() => {
createCommunityMessenger(communityKey, addMessage, identity).then( createMessenger(identity).then((e) => {
(result) => { setMessenger(e);
setCommunity(result.community); });
setMessenger(result.messenger);
}
);
}, []); }, []);
useEffect(() => {
if (messenger && contactsClass) {
createCommunity(communityKey, addMessage, messenger).then((e) => {
setCommunity(e);
});
}
}, [messenger, communityKey, addMessage, contactsClass]);
useEffect(() => { useEffect(() => {
if (messenger && community?.chats) { if (messenger && community?.chats) {
Array.from(community?.chats.values()).forEach(({ id }) => Array.from(community?.chats.values()).forEach(({ id }) =>
loadPrevDay(id) loadPrevDay(id)
); );
} }
}, [messenger]); }, [messenger, community]);
const sendMessage = useCallback( const sendMessage = useCallback(
async (messageText?: string, image?: Uint8Array) => { async (messageText?: string, image?: Uint8Array) => {
@ -63,5 +109,6 @@ export function useMessenger(
loadPrevDay, loadPrevDay,
loadingMessages, loadingMessages,
community, community,
contacts,
}; };
} }

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export function useRefBreak(dimension: number, sizeThreshold: number) { export function useRefBreak(dimension: number, sizeThreshold: number) {
const [widthBreak, setWidthBreak] = useState(false); const [widthBreak, setWidthBreak] = useState(dimension < sizeThreshold);
useEffect(() => { useEffect(() => {
const checkDimensions = () => { const checkDimensions = () => {

View File

@ -1,4 +1,4 @@
import { ApplicationMetadataMessage } from "status-communities/dist/cjs"; import { ApplicationMetadataMessage, utils } from "status-communities/dist/cjs";
import { uintToImgUrl } from "../utils"; import { uintToImgUrl } from "../utils";
@ -29,10 +29,7 @@ export class ChatMessage {
if (msg.chatMessage?.image) { if (msg.chatMessage?.image) {
image = uintToImgUrl(msg.chatMessage?.image.payload); image = uintToImgUrl(msg.chatMessage?.image.payload);
} }
const sender = msg.signer.reduce( const sender = utils.bufToHex(msg.signer);
(p: string, c: number): string => p + c.toString(16),
"0x"
);
return new ChatMessage(content, date, sender, image); return new ChatMessage(content, date, sender, image);
} else { } else {
return undefined; return undefined;

View File

@ -0,0 +1,4 @@
export type Contact = {
id: string;
online: boolean;
};

View File

@ -0,0 +1,23 @@
import { Community, Messenger } from "status-communities/dist/cjs";
import { ApplicationMetadataMessage } from "status-communities/dist/cjs";
export async function createCommunity(
communityKey: string,
addMessage: (msg: ApplicationMetadataMessage, id: string, date: Date) => void,
messenger: Messenger
) {
const community = await Community.instantiateCommunity(
communityKey,
messenger.waku
);
await Promise.all(
Array.from(community.chats.values()).map(async (chat) => {
await messenger.joinChat(chat);
messenger.addObserver(
(msg, date) => addMessage(msg, chat.id, date),
chat.id
);
})
);
return community;
}

View File

@ -1,44 +0,0 @@
import { StoreCodec } from "js-waku";
import { Community, Identity, Messenger } from "status-communities/dist/cjs";
import { ApplicationMetadataMessage } from "status-communities/dist/cjs";
const WAKU_OPTIONS = {
libp2p: {
config: {
pubsub: {
enabled: true,
emitSelf: true,
},
},
},
};
export async function createCommunityMessenger(
communityKey: string,
addMessage: (msg: ApplicationMetadataMessage, id: string, date: Date) => void,
identity: Identity
) {
const messenger = await Messenger.create(identity, WAKU_OPTIONS);
await new Promise((resolve) => {
messenger.waku.libp2p.peerStore.on("change:protocols", ({ protocols }) => {
if (protocols.includes(StoreCodec)) {
resolve("");
}
});
});
const community = await Community.instantiateCommunity(
communityKey,
messenger.waku
);
await Promise.all(
Array.from(community.chats.values()).map(async (chat) => {
await messenger.joinChat(chat);
messenger.addObserver(
(msg, date) => addMessage(msg, chat.id, date),
chat.id
);
})
);
return { messenger, community, identity };
}

View File

@ -0,0 +1,26 @@
import { StoreCodec } from "js-waku";
import { Identity, Messenger } from "status-communities/dist/cjs";
const WAKU_OPTIONS = {
libp2p: {
config: {
pubsub: {
enabled: true,
emitSelf: true,
},
},
},
};
export async function createMessenger(identity: Identity) {
const messenger = await Messenger.create(identity, WAKU_OPTIONS);
await new Promise((resolve) => {
messenger.waku.libp2p.peerStore.on("change:protocols", ({ protocols }) => {
if (protocols.includes(StoreCodec)) {
resolve("");
}
});
});
return messenger;
}

View File

@ -0,0 +1,32 @@
syntax = "proto3";
package communities.v1;
/* Specs:
:AUTOMATIC
To Send - "AUTOMATIC" status ping every 5 minutes
Display - Online for up to 5 minutes from the last clock, after that Offline
:ALWAYS_ONLINE
To Send - "ALWAYS_ONLINE" status ping every 5 minutes
Display - Online for up to 2 weeks from the last clock, after that Offline
:INACTIVE
To Send - A single "INACTIVE" status ping
Display - Offline forever
Note: Only send pings if the user interacted with the app in the last x minutes. */
message StatusUpdate {
uint64 clock = 1;
StatusType status_type = 2;
string custom_text = 3;
enum StatusType {
UNKNOWN_STATUS_TYPE = 0;
AUTOMATIC = 1;
DO_NOT_DISTURB = 2;
ALWAYS_ONLINE = 3;
INACTIVE = 4;
};
}

View File

@ -0,0 +1,63 @@
import { Waku, WakuMessage } from "js-waku";
import { idToContactCodeTopic } from "./contentTopic";
import { Identity } from "./identity";
import { StatusUpdate_StatusType } from "./proto/communities/v1/status_update";
import { bufToHex } from "./utils";
import { StatusUpdate } from "./wire/status_update";
export class Contacts {
waku: Waku;
identity: Identity;
private callback: (id: string, clock: number) => void;
private contacts: string[] = [];
public constructor(
identity: Identity,
waku: Waku,
callback: (id: string, clock: number) => void
) {
this.waku = waku;
this.identity = identity;
this.callback = callback;
this.startBroadcast();
}
public addContact(id: string): void {
if (!this.contacts.find((e) => id === e)) {
const now = new Date();
const callback = (wakuMessage: WakuMessage): void => {
if (wakuMessage.payload) {
const msg = StatusUpdate.decode(wakuMessage.payload);
this.callback(id, msg.clock ?? 0);
}
};
this.contacts.push(id);
this.callback(id, 0);
this.waku.store.queryHistory([idToContactCodeTopic(id)], {
callback: (msgs) => msgs.forEach((e) => callback(e)),
timeFilter: {
startTime: new Date(now.getTime() - 400000),
endTime: now,
},
});
this.waku.relay.addObserver(callback, [idToContactCodeTopic(id)]);
}
}
private startBroadcast(): void {
const send = async (): Promise<void> => {
const statusUpdate = StatusUpdate.create(
StatusUpdate_StatusType.AUTOMATIC,
""
);
const msg = await WakuMessage.fromBytes(
statusUpdate.encode(),
idToContactCodeTopic(bufToHex(this.identity.publicKey))
);
this.waku.relay.send(msg);
};
send();
setInterval(send, 300000);
}
}

View File

@ -16,3 +16,7 @@ export function idToContentTopic(id: string): string {
return "/waku/1/" + "0x" + topic.toString("hex") + "/rfc26"; return "/waku/1/" + "0x" + topic.toString("hex") + "/rfc26";
} }
export function idToContactCodeTopic(id: string): string {
return idToContentTopic(id + "-contact-code");
}

View File

@ -1,6 +1,7 @@
export { Identity } from "./identity"; export { Identity } from "./identity";
export { Messenger } from "./messenger"; export { Messenger } from "./messenger";
export { Community } from "./community"; export { Community } from "./community";
export { Contacts } from "./contacts";
export { Chat } from "./chat"; export { Chat } from "./chat";
export * as utils from "./utils"; export * as utils from "./utils";
export { ApplicationMetadataMessage } from "./wire/application_metadata_message"; export { ApplicationMetadataMessage } from "./wire/application_metadata_message";

View File

@ -0,0 +1,212 @@
/* eslint-disable */
import Long from "long";
import _m0 from "protobufjs/minimal";
export const protobufPackage = "communities.v1";
/**
* Specs:
* :AUTOMATIC
* To Send - "AUTOMATIC" status ping every 5 minutes
* Display - Online for up to 5 minutes from the last clock, after that Offline
* :ALWAYS_ONLINE
* To Send - "ALWAYS_ONLINE" status ping every 5 minutes
* Display - Online for up to 2 weeks from the last clock, after that Offline
* :INACTIVE
* To Send - A single "INACTIVE" status ping
* Display - Offline forever
* Note: Only send pings if the user interacted with the app in the last x minutes.
*/
export interface StatusUpdate {
clock: number;
statusType: StatusUpdate_StatusType;
customText: string;
}
export enum StatusUpdate_StatusType {
UNKNOWN_STATUS_TYPE = 0,
AUTOMATIC = 1,
DO_NOT_DISTURB = 2,
ALWAYS_ONLINE = 3,
INACTIVE = 4,
UNRECOGNIZED = -1,
}
export function statusUpdate_StatusTypeFromJSON(
object: any
): StatusUpdate_StatusType {
switch (object) {
case 0:
case "UNKNOWN_STATUS_TYPE":
return StatusUpdate_StatusType.UNKNOWN_STATUS_TYPE;
case 1:
case "AUTOMATIC":
return StatusUpdate_StatusType.AUTOMATIC;
case 2:
case "DO_NOT_DISTURB":
return StatusUpdate_StatusType.DO_NOT_DISTURB;
case 3:
case "ALWAYS_ONLINE":
return StatusUpdate_StatusType.ALWAYS_ONLINE;
case 4:
case "INACTIVE":
return StatusUpdate_StatusType.INACTIVE;
case -1:
case "UNRECOGNIZED":
default:
return StatusUpdate_StatusType.UNRECOGNIZED;
}
}
export function statusUpdate_StatusTypeToJSON(
object: StatusUpdate_StatusType
): string {
switch (object) {
case StatusUpdate_StatusType.UNKNOWN_STATUS_TYPE:
return "UNKNOWN_STATUS_TYPE";
case StatusUpdate_StatusType.AUTOMATIC:
return "AUTOMATIC";
case StatusUpdate_StatusType.DO_NOT_DISTURB:
return "DO_NOT_DISTURB";
case StatusUpdate_StatusType.ALWAYS_ONLINE:
return "ALWAYS_ONLINE";
case StatusUpdate_StatusType.INACTIVE:
return "INACTIVE";
default:
return "UNKNOWN";
}
}
const baseStatusUpdate: object = { clock: 0, statusType: 0, customText: "" };
export const StatusUpdate = {
encode(
message: StatusUpdate,
writer: _m0.Writer = _m0.Writer.create()
): _m0.Writer {
if (message.clock !== 0) {
writer.uint32(8).uint64(message.clock);
}
if (message.statusType !== 0) {
writer.uint32(16).int32(message.statusType);
}
if (message.customText !== "") {
writer.uint32(26).string(message.customText);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): StatusUpdate {
const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = { ...baseStatusUpdate } as StatusUpdate;
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
message.clock = longToNumber(reader.uint64() as Long);
break;
case 2:
message.statusType = reader.int32() as any;
break;
case 3:
message.customText = reader.string();
break;
default:
reader.skipType(tag & 7);
break;
}
}
return message;
},
fromJSON(object: any): StatusUpdate {
const message = { ...baseStatusUpdate } as StatusUpdate;
if (object.clock !== undefined && object.clock !== null) {
message.clock = Number(object.clock);
} else {
message.clock = 0;
}
if (object.statusType !== undefined && object.statusType !== null) {
message.statusType = statusUpdate_StatusTypeFromJSON(object.statusType);
} else {
message.statusType = 0;
}
if (object.customText !== undefined && object.customText !== null) {
message.customText = String(object.customText);
} else {
message.customText = "";
}
return message;
},
toJSON(message: StatusUpdate): unknown {
const obj: any = {};
message.clock !== undefined && (obj.clock = message.clock);
message.statusType !== undefined &&
(obj.statusType = statusUpdate_StatusTypeToJSON(message.statusType));
message.customText !== undefined && (obj.customText = message.customText);
return obj;
},
fromPartial(object: DeepPartial<StatusUpdate>): StatusUpdate {
const message = { ...baseStatusUpdate } as StatusUpdate;
if (object.clock !== undefined && object.clock !== null) {
message.clock = object.clock;
} else {
message.clock = 0;
}
if (object.statusType !== undefined && object.statusType !== null) {
message.statusType = object.statusType;
} else {
message.statusType = 0;
}
if (object.customText !== undefined && object.customText !== null) {
message.customText = object.customText;
} else {
message.customText = "";
}
return message;
},
};
declare var self: any | undefined;
declare var window: any | undefined;
declare var global: any | undefined;
var globalThis: any = (() => {
if (typeof globalThis !== "undefined") return globalThis;
if (typeof self !== "undefined") return self;
if (typeof window !== "undefined") return window;
if (typeof global !== "undefined") return global;
throw "Unable to locate global object";
})();
type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined;
export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
function longToNumber(long: Long): number {
if (long.gt(Number.MAX_SAFE_INTEGER)) {
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
}
return long.toNumber();
}
if (_m0.util.Long !== Long) {
_m0.util.Long = Long as any;
_m0.configure();
}

View File

@ -0,0 +1,49 @@
import { Reader } from "protobufjs";
import * as proto from "../proto/communities/v1/status_update";
export class StatusUpdate {
public constructor(public proto: proto.StatusUpdate) {}
public static create(
statusType: proto.StatusUpdate_StatusType,
customText: string
): StatusUpdate {
const clock = Date.now();
const proto = {
clock,
statusType,
customText,
};
return new StatusUpdate(proto);
}
/**
* Decode the payload as CommunityChat message.
*
* @throws
*/
static decode(bytes: Uint8Array): StatusUpdate {
const protoBuf = proto.StatusUpdate.decode(Reader.create(bytes));
return new StatusUpdate(protoBuf);
}
encode(): Uint8Array {
return proto.StatusUpdate.encode(this.proto).finish();
}
public get clock(): number | undefined {
return this.proto.clock;
}
public get statusType(): proto.StatusUpdate_StatusType | undefined {
return this.proto.statusType;
}
public get customText(): string | undefined {
return this.proto.customText;
}
}