Introduce online status broadcast (#99)
This commit is contained in:
parent
f957aa76cf
commit
7c3f256e61
|
@ -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 ReactDOM from "react-dom";
|
||||
import styled from "styled-components";
|
||||
|
@ -32,6 +32,8 @@ function DragDiv() {
|
|||
const moved = useRef(false);
|
||||
const setting = useRef("");
|
||||
|
||||
const [theme, setTheme] = useState(true);
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (setting.current === "position") {
|
||||
e.preventDefault();
|
||||
|
@ -54,6 +56,14 @@ function DragDiv() {
|
|||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setTheme(!theme);
|
||||
}}
|
||||
>
|
||||
Change theme
|
||||
</button>
|
||||
<Drag style={{ left: x, top: y, width: width, height: height }} ref={ref}>
|
||||
<Bubble
|
||||
onMouseDown={() => {
|
||||
|
@ -64,7 +74,7 @@ function DragDiv() {
|
|||
/>
|
||||
<FloatingDiv className={showChat ? "" : "hide"}>
|
||||
<ReactChat
|
||||
theme={lightTheme}
|
||||
theme={theme ? lightTheme : darkTheme}
|
||||
communityKey={
|
||||
"0x0262c65c881f5a9f79343a26faaa02aad3af7c533d9445fb1939ed11b8bf4d2abd"
|
||||
}
|
||||
|
@ -81,6 +91,7 @@ function DragDiv() {
|
|||
></SizeSet>
|
||||
)}
|
||||
</Drag>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ export function Chat({
|
|||
loadPrevDay,
|
||||
loadingMessages,
|
||||
community,
|
||||
contacts,
|
||||
} = useMessenger(activeChannel?.id ?? "", communityKey, identity);
|
||||
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
@ -68,14 +69,7 @@ export function Chat({
|
|||
description: community.description.identity?.description ?? "",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
id: 1,
|
||||
name: "",
|
||||
icon: "",
|
||||
members: 0,
|
||||
membersList: [],
|
||||
description: "",
|
||||
};
|
||||
return undefined;
|
||||
}
|
||||
}, [community]);
|
||||
|
||||
|
@ -101,7 +95,7 @@ export function Chat({
|
|||
<ChatWrapper>
|
||||
{showChannels && !narrow && (
|
||||
<ChannelsWrapper>
|
||||
{messenger ? (
|
||||
{community && communityData ? (
|
||||
<StyledCommunity onClick={showModal} community={communityData} />
|
||||
) : (
|
||||
<CommunitySkeleton />
|
||||
|
@ -117,6 +111,8 @@ export function Chat({
|
|||
</ChannelsWrapper>
|
||||
)}
|
||||
<ChatBody
|
||||
identity={identity}
|
||||
contacts={contacts}
|
||||
theme={theme}
|
||||
channel={activeChannel}
|
||||
messenger={messenger}
|
||||
|
@ -140,11 +136,13 @@ export function Chat({
|
|||
/>
|
||||
{showMembers && !narrow && (
|
||||
<Members
|
||||
community={communityData}
|
||||
identity={identity}
|
||||
contacts={contacts}
|
||||
setShowChannels={setShowChannels}
|
||||
setMembersList={setMembersList}
|
||||
/>
|
||||
)}
|
||||
{communityData && (
|
||||
<CommunityModal
|
||||
isVisible={isModalVisible}
|
||||
onClose={() => setIsModalVisible(false)}
|
||||
|
@ -154,6 +152,7 @@ export function Chat({
|
|||
description={communityData.description}
|
||||
publicKey={communityKey}
|
||||
/>
|
||||
)}
|
||||
</ChatWrapper>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Identity } from "status-communities/dist/cjs";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { useNarrow } from "../../contexts/narrowProvider";
|
||||
import { ChannelData } from "../../models/ChannelData";
|
||||
import { ChatMessage } from "../../models/ChatMessage";
|
||||
import { CommunityData } from "../../models/CommunityData";
|
||||
import { Contact } from "../../models/Contact";
|
||||
import { Metadata } from "../../models/Metadata";
|
||||
import { Theme } from "../../styles/themes";
|
||||
import { Channel } from "../Channels/Channel";
|
||||
|
@ -21,9 +23,11 @@ import { ChatInput } from "./ChatInput";
|
|||
import { ChatMessages } from "./ChatMessages";
|
||||
|
||||
interface ChatBodyProps {
|
||||
identity: Identity;
|
||||
contacts: Contact[];
|
||||
theme: Theme;
|
||||
channel: ChannelData;
|
||||
community: CommunityData;
|
||||
community: CommunityData | undefined;
|
||||
messenger: any;
|
||||
messages: ChatMessage[];
|
||||
sendMessage: (text: string, image?: Uint8Array) => void;
|
||||
|
@ -44,6 +48,8 @@ interface ChatBodyProps {
|
|||
}
|
||||
|
||||
export function ChatBody({
|
||||
identity,
|
||||
contacts,
|
||||
theme,
|
||||
channel,
|
||||
community,
|
||||
|
@ -95,7 +101,7 @@ export function ChatBody({
|
|||
}
|
||||
>
|
||||
<ChannelWrapper className={className}>
|
||||
{messenger ? (
|
||||
{messenger && community ? (
|
||||
<>
|
||||
{(showCommunity || narrow) && (
|
||||
<CommunityWrap className={className}>
|
||||
|
@ -125,14 +131,14 @@ export function ChatBody({
|
|||
>
|
||||
<MembersIcon />
|
||||
</MemberBtn>
|
||||
{!messenger && <Loading />}
|
||||
{!community && <Loading />}
|
||||
</ChatTopbar>
|
||||
{messenger ? (
|
||||
{messenger && community ? (
|
||||
<>
|
||||
{!showChannelsList && !showMembersList && (
|
||||
<>
|
||||
{messages.length > 0 ? (
|
||||
messenger ? (
|
||||
messenger && community ? (
|
||||
<ChatMessages
|
||||
messages={messages}
|
||||
loadPrevDay={loadPrevDay}
|
||||
|
@ -163,6 +169,8 @@ export function ChatBody({
|
|||
)}
|
||||
{showMembersList && narrow && (
|
||||
<NarrowMembers
|
||||
identity={identity}
|
||||
contacts={contacts}
|
||||
community={community}
|
||||
setShowChannels={setShowChannelsList}
|
||||
setShowMembersList={setShowMembersList}
|
||||
|
|
|
@ -23,6 +23,7 @@ export function ChatInput({ theme, addMessage }: ChatInputProps) {
|
|||
const [inputHeight, setInputHeight] = useState(40);
|
||||
const [imageUint, setImageUint] = useState<undefined | Uint8Array>(undefined);
|
||||
const [showSizeLimit, setShowSizeLimit] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("click", () => setShowEmoji(false));
|
||||
return () => {
|
||||
|
@ -127,11 +128,9 @@ export function ChatInput({ theme, addMessage }: ChatInputProps) {
|
|||
/>
|
||||
</AddPictureInputWrapper>
|
||||
<Row style={{ height: `${inputHeight + (image ? 73 : 0)}px` }}>
|
||||
<InputWrapper>
|
||||
{image && (
|
||||
<ImagePreviewWrapper>
|
||||
<ImagePreviewOverlay />
|
||||
<ImagePreview src={image} onClick={() => setImageUint(undefined)} />
|
||||
</ImagePreviewWrapper>
|
||||
)}
|
||||
<Input
|
||||
placeholder="Message"
|
||||
|
@ -139,6 +138,7 @@ export function ChatInput({ theme, addMessage }: ChatInputProps) {
|
|||
onChange={onInputChange}
|
||||
onKeyPress={onInputKeyPress}
|
||||
/>
|
||||
</InputWrapper>
|
||||
<InputButtons>
|
||||
<ChatButton
|
||||
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`
|
||||
display: flex;
|
||||
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`
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px 16px 4px 16px;
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import React from "react";
|
||||
import { Identity } from "status-communities/dist/cjs";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { CommunityData } from "../../models/CommunityData";
|
||||
import { Contact } from "../../models/Contact";
|
||||
|
||||
import { MembersList } from "./MembersList";
|
||||
|
||||
interface MembersProps {
|
||||
community: CommunityData;
|
||||
identity: Identity;
|
||||
contacts: Contact[];
|
||||
setShowChannels: (val: boolean) => void;
|
||||
setMembersList: any;
|
||||
}
|
||||
|
||||
export function Members({
|
||||
community,
|
||||
identity,
|
||||
contacts,
|
||||
setShowChannels,
|
||||
setMembersList,
|
||||
}: MembersProps) {
|
||||
|
@ -20,7 +23,8 @@ export function Members({
|
|||
<MembersWrapper>
|
||||
<MemberHeading>Members</MemberHeading>
|
||||
<MembersList
|
||||
community={community}
|
||||
identity={identity}
|
||||
contacts={contacts}
|
||||
setShowChannels={setShowChannels}
|
||||
setMembersList={setMembersList}
|
||||
/>
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
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 { CommunityData } from "../../models/CommunityData";
|
||||
import { Contact } from "../../models/Contact";
|
||||
import { UserIcon } from "../Icons/UserIcon";
|
||||
|
||||
import { Member, MemberData, MemberIcon } from "./Member";
|
||||
|
||||
interface MembersListProps {
|
||||
community: CommunityData;
|
||||
identity: Identity;
|
||||
contacts: Contact[];
|
||||
setShowChannels: (val: boolean) => void;
|
||||
setShowMembers?: (val: boolean) => void;
|
||||
setMembersList: any;
|
||||
}
|
||||
|
||||
export function MembersList({
|
||||
community,
|
||||
identity,
|
||||
contacts,
|
||||
setShowChannels,
|
||||
setShowMembers,
|
||||
setMembersList,
|
||||
|
@ -27,18 +31,19 @@ export function MembersList({
|
|||
<MemberIcon>
|
||||
<UserIcon memberView={true} />
|
||||
</MemberIcon>
|
||||
<MemberName>Guest564732</MemberName>
|
||||
<MemberName>{utils.bufToHex(identity.publicKey)}</MemberName>
|
||||
</MemberData>
|
||||
</MemberCategory>
|
||||
<MemberCategory>
|
||||
<MemberCategoryName>Online</MemberCategoryName>
|
||||
{community.membersList
|
||||
.filter(() => false)
|
||||
.map((member) => (
|
||||
{contacts
|
||||
.filter((e) => e.id != bufToHex(identity.publicKey))
|
||||
.filter((e) => e.online)
|
||||
.map((contact) => (
|
||||
<Member
|
||||
key={member}
|
||||
member={member}
|
||||
isOnline={false}
|
||||
key={contact.id}
|
||||
member={contact.id}
|
||||
isOnline={contact.online}
|
||||
setShowChannels={setShowChannels}
|
||||
setShowMembers={setShowMembers}
|
||||
setMembersList={setMembersList}
|
||||
|
@ -47,11 +52,14 @@ export function MembersList({
|
|||
</MemberCategory>
|
||||
<MemberCategory>
|
||||
<MemberCategoryName>Offline</MemberCategoryName>
|
||||
{community.membersList.map((member) => (
|
||||
{contacts
|
||||
.filter((e) => e.id != bufToHex(identity.publicKey))
|
||||
.filter((e) => !e.online)
|
||||
.map((contact) => (
|
||||
<Member
|
||||
key={member}
|
||||
member={member}
|
||||
isOnline={false}
|
||||
key={contact.id}
|
||||
member={contact.id}
|
||||
isOnline={contact.online}
|
||||
setShowChannels={setShowChannels}
|
||||
setShowMembers={setShowMembers}
|
||||
setMembersList={setMembersList}
|
||||
|
|
|
@ -1,20 +1,26 @@
|
|||
import React from "react";
|
||||
import { Identity } from "status-communities/dist/cjs";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { CommunityData } from "../../models/CommunityData";
|
||||
import { Contact } from "../../models/Contact";
|
||||
import { MembersList } from "../Members/MembersList";
|
||||
|
||||
import { NarrowTopbar } from "./NarrowTopbar";
|
||||
|
||||
interface NarrowMembersProps {
|
||||
identity: Identity;
|
||||
community: CommunityData;
|
||||
contacts: Contact[];
|
||||
setShowChannels: (val: boolean) => void;
|
||||
setShowMembersList: (val: boolean) => void;
|
||||
setMembersList: any;
|
||||
}
|
||||
|
||||
export function NarrowMembers({
|
||||
identity,
|
||||
community,
|
||||
contacts,
|
||||
setShowChannels,
|
||||
setShowMembersList,
|
||||
setMembersList,
|
||||
|
@ -23,7 +29,8 @@ export function NarrowMembers({
|
|||
<ListWrapper>
|
||||
<NarrowTopbar list="Community members" community={community.name} />
|
||||
<MembersList
|
||||
community={community}
|
||||
identity={identity}
|
||||
contacts={contacts}
|
||||
setShowChannels={setShowChannels}
|
||||
setShowMembers={setShowMembersList}
|
||||
setMembersList={setMembersList}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
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 { binarySetInsert } from "../../utils";
|
||||
|
||||
import { useNotifications } from "./useNotifications";
|
||||
|
||||
export function useMessages(chatId: string) {
|
||||
export function useMessages(chatId: string, contacts?: Contacts) {
|
||||
const [messages, setMessages] = useState<{ [chatId: string]: ChatMessage[] }>(
|
||||
{}
|
||||
);
|
||||
|
@ -17,6 +20,9 @@ export function useMessages(chatId: string) {
|
|||
(msg: ApplicationMetadataMessage, id: string, date: Date) => {
|
||||
const newMessage = ChatMessage.fromMetadataMessage(msg, date);
|
||||
if (newMessage) {
|
||||
if (contacts) {
|
||||
contacts.addContact(newMessage.sender);
|
||||
}
|
||||
setMessages((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
|
@ -31,7 +37,7 @@ export function useMessages(chatId: string) {
|
|||
incNotification(id);
|
||||
}
|
||||
},
|
||||
[]
|
||||
[contacts]
|
||||
);
|
||||
|
||||
const activeMessages = useMemo(
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
// import { StoreCodec } from "js-waku";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Community, Identity, Messenger } from "status-communities/dist/cjs";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
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 { useMessages } from "./useMessages";
|
||||
|
@ -13,27 +21,65 @@ export function useMessenger(
|
|||
identity: Identity
|
||||
) {
|
||||
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 } =
|
||||
useMessages(chatId);
|
||||
useMessages(chatId, contactsClass);
|
||||
const [community, setCommunity] = useState<Community | undefined>(undefined);
|
||||
const { loadPrevDay, loadingMessages } = useLoadPrevDay(chatId, messenger);
|
||||
|
||||
useEffect(() => {
|
||||
createCommunityMessenger(communityKey, addMessage, identity).then(
|
||||
(result) => {
|
||||
setCommunity(result.community);
|
||||
setMessenger(result.messenger);
|
||||
}
|
||||
);
|
||||
createMessenger(identity).then((e) => {
|
||||
setMessenger(e);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (messenger && contactsClass) {
|
||||
createCommunity(communityKey, addMessage, messenger).then((e) => {
|
||||
setCommunity(e);
|
||||
});
|
||||
}
|
||||
}, [messenger, communityKey, addMessage, contactsClass]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messenger && community?.chats) {
|
||||
Array.from(community?.chats.values()).forEach(({ id }) =>
|
||||
loadPrevDay(id)
|
||||
);
|
||||
}
|
||||
}, [messenger]);
|
||||
}, [messenger, community]);
|
||||
|
||||
const sendMessage = useCallback(
|
||||
async (messageText?: string, image?: Uint8Array) => {
|
||||
|
@ -63,5 +109,6 @@ export function useMessenger(
|
|||
loadPrevDay,
|
||||
loadingMessages,
|
||||
community,
|
||||
contacts,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useRefBreak(dimension: number, sizeThreshold: number) {
|
||||
const [widthBreak, setWidthBreak] = useState(false);
|
||||
const [widthBreak, setWidthBreak] = useState(dimension < sizeThreshold);
|
||||
|
||||
useEffect(() => {
|
||||
const checkDimensions = () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ApplicationMetadataMessage } from "status-communities/dist/cjs";
|
||||
import { ApplicationMetadataMessage, utils } from "status-communities/dist/cjs";
|
||||
|
||||
import { uintToImgUrl } from "../utils";
|
||||
|
||||
|
@ -29,10 +29,7 @@ export class ChatMessage {
|
|||
if (msg.chatMessage?.image) {
|
||||
image = uintToImgUrl(msg.chatMessage?.image.payload);
|
||||
}
|
||||
const sender = msg.signer.reduce(
|
||||
(p: string, c: number): string => p + c.toString(16),
|
||||
"0x"
|
||||
);
|
||||
const sender = utils.bufToHex(msg.signer);
|
||||
return new ChatMessage(content, date, sender, image);
|
||||
} else {
|
||||
return undefined;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export type Contact = {
|
||||
id: string;
|
||||
online: boolean;
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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 };
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -16,3 +16,7 @@ export function idToContentTopic(id: string): string {
|
|||
|
||||
return "/waku/1/" + "0x" + topic.toString("hex") + "/rfc26";
|
||||
}
|
||||
|
||||
export function idToContactCodeTopic(id: string): string {
|
||||
return idToContentTopic(id + "-contact-code");
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export { Identity } from "./identity";
|
||||
export { Messenger } from "./messenger";
|
||||
export { Community } from "./community";
|
||||
export { Contacts } from "./contacts";
|
||||
export { Chat } from "./chat";
|
||||
export * as utils from "./utils";
|
||||
export { ApplicationMetadataMessage } from "./wire/application_metadata_message";
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue