Add members (#271)

* export ColorHash type

* add getObjectsDifference helper

* fix CommunityDescription generated type

* add members to community module

* use members in sidebar
This commit is contained in:
Pavel 2022-06-13 19:02:37 +02:00 committed by GitHub
parent 062c29d6fa
commit 5b4daedd37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 137 additions and 24 deletions

View File

@ -114,13 +114,12 @@ export namespace CommunityPermissions {
export interface CommunityDescription { export interface CommunityDescription {
clock: bigint clock: bigint
members: CommunityMember members: Record<string, CommunityMember>
permissions: CommunityPermissions permissions: CommunityPermissions
identity: ChatIdentity identity: ChatIdentity
// fixme!: Map chats: Record<string, CommunityChat>
chats: CommunityChat
banList: string[] banList: string[]
categories: CommunityCategory categories: Record<string, CommunityCategory>
archiveMagnetlinkClock: bigint archiveMagnetlinkClock: bigint
adminSettings: CommunityAdminSettings adminSettings: CommunityAdminSettings
} }

View File

@ -2,10 +2,12 @@ import { waku_message } from 'js-waku'
import { MessageType } from '~/protos/enums' import { MessageType } from '~/protos/enums'
import { getDifferenceByKeys } from '~/src/helpers/get-difference-by-keys' import { getDifferenceByKeys } from '~/src/helpers/get-difference-by-keys'
import { getObjectsDifference } from '~/src/helpers/get-objects-difference'
import { idToContentTopic } from '../../contentTopic' import { idToContentTopic } from '../../contentTopic'
import { createSymKeyFromPassword } from '../../encryption' import { createSymKeyFromPassword } from '../../encryption'
import { Chat } from '../chat' import { Chat } from '../chat'
import { Member } from '../member'
import type { Client } from '../../client' import type { Client } from '../../client'
import type { import type {
@ -21,6 +23,7 @@ export class Community {
private symmetricKey!: Uint8Array private symmetricKey!: Uint8Array
public description!: CommunityDescription public description!: CommunityDescription
public chats: Map<string, Chat> public chats: Map<string, Chat>
#members: Map<string, Member>
public callback: ((description: CommunityDescription) => void) | undefined public callback: ((description: CommunityDescription) => void) | undefined
constructor(client: Client, publicKey: string) { constructor(client: Client, publicKey: string) {
@ -28,6 +31,7 @@ export class Community {
this.publicKey = publicKey this.publicKey = publicKey
this.chats = new Map() this.chats = new Map()
this.#members = new Map()
} }
public async start() { public async start() {
@ -52,6 +56,7 @@ export class Community {
this.description = description this.description = description
this.observe() this.observe()
this.addMembers(this.description.members)
// Chats // Chats
await this.observeChatMessages(this.description.chats) await this.observeChatMessages(this.description.chats)
@ -62,6 +67,18 @@ export class Community {
return [...this.chats.values()] return [...this.chats.values()]
} }
public getChat(uuid: string) {
return this.chats.get(uuid)
}
public get members() {
return [...this.#members.values()]
}
public getMember(publicKey: string) {
return this.#members.get(publicKey)
}
public fetch = async () => { public fetch = async () => {
await this.client.waku.store.queryHistory([this.contentTopic], { await this.client.waku.store.queryHistory([this.contentTopic], {
// oldest message first // oldest message first
@ -137,6 +154,19 @@ export class Community {
) )
} }
private addMembers = (members: CommunityDescription['members']) => {
for (const publicKey of Object.keys(members)) {
const member = new Member(publicKey)
this.#members.set(publicKey, member)
}
}
private removeMembers = (ids: string[]) => {
for (const id of ids) {
this.#members.delete(id)
}
}
public handleDescription = (description: CommunityDescription) => { public handleDescription = (description: CommunityDescription) => {
if (this.description) { if (this.description) {
// already handled // already handled
@ -161,6 +191,23 @@ export class Community {
if (Object.keys(addedChats).length) { if (Object.keys(addedChats).length) {
this.observeChatMessages(addedChats) this.observeChatMessages(addedChats)
} }
// TODO: migrate chats to new format
// const chats = getObjectsDifference(
// this.description.chats,
// description.chats
// )
// this.observeChatMessages(chats.added)
// this.unobserveChatMessages(chats.removed)
const members = getObjectsDifference(
this.description.members,
description.members
)
this.addMembers(members.added)
this.removeMembers(members.removed)
} }
// Community // Community

View File

@ -0,0 +1,17 @@
import { generateUsername } from '../utils/generate-username'
import { publicKeyToColorHash } from '../utils/public-key-to-color-hash'
import type { ColorHash } from '../utils/public-key-to-color-hash'
export class Member {
publicKey: string
username: string
colorHash: ColorHash
constructor(publicKey: string) {
this.publicKey = publicKey
this.username = generateUsername(publicKey)
// TODO: can it fail?
this.colorHash = publicKeyToColorHash(publicKey)!
}
}

View File

@ -0,0 +1,25 @@
import { getObjectsDifference } from './get-objects-difference'
describe('getObjectsDifference', () => {
it('returns correct difference', () => {
const oldObject = { a: 1, b: 2, c: 3 }
const newObject = { c: 3, d: 4, e: 5 }
expect(getObjectsDifference(oldObject, newObject)).toEqual({
added: {
d: 4,
e: 5,
},
removed: ['a', 'b'],
})
})
it('returns empty arrays for the same object', () => {
const object = { a: 1, b: 2, c: 3 }
expect(getObjectsDifference(object, object)).toEqual({
added: {},
removed: [],
})
})
})

View File

@ -0,0 +1,23 @@
type Input<Value> = Record<string, Value>
export function getObjectsDifference<Value>(
oldObject: Input<Value>,
newObject: Input<Value>
) {
const added: Record<string, Value> = {}
const removed: string[] = []
for (const key of Object.keys(oldObject)) {
if (!newObject[key]) {
removed.push(key)
}
}
for (const key of Object.keys(newObject)) {
if (!oldObject[key]) {
added[key] = newObject[key]
}
}
return { added, removed }
}

View File

@ -3,3 +3,4 @@ export type { Client, ClientOptions } from './client'
export { createClient } from './client' export { createClient } from './client'
export type { ChatMessage as Message } from './client/chat' export type { ChatMessage as Message } from './client/chat'
export type { Community } from './client/community/community' export type { Community } from './client/community/community'
export type { Member } from './client/member'

View File

@ -1,6 +1,6 @@
import * as secp256k1 from 'ethereum-cryptography/secp256k1' import * as secp256k1 from 'ethereum-cryptography/secp256k1'
type ColorHash = number[][] export type ColorHash = number[][]
const COLOR_HASH_COLORS_COUNT = 32 const COLOR_HASH_COLORS_COUNT = 32
const COLOR_HASH_SEGMENT_MAX_LENGTH = 5 const COLOR_HASH_SEGMENT_MAX_LENGTH = 5

View File

@ -9,8 +9,8 @@ import { MemberItem } from './member-item'
import { UserItem } from './user-item' import { UserItem } from './user-item'
export function MemberSidebar() { export function MemberSidebar() {
const members = useMembers()
const { account } = useAccount() const { account } = useAccount()
const members = useMembers()
return ( return (
<Wrapper> <Wrapper>
@ -26,14 +26,12 @@ export function MemberSidebar() {
<MemberGroup label="Online"> <MemberGroup label="Online">
{members.map(member => ( {members.map(member => (
<MemberItem <MemberItem
key={member} key={member.publicKey}
verified={false} verified={false}
untrustworthy={false} untrustworthy={false}
indicator="online" // indicator=""
chatKey={member} member={member}
> />
{member}
</MemberItem>
))} ))}
</MemberGroup> </MemberGroup>
{/* <MemberGroup label="Offline"></MemberGroup> */} {/* <MemberGroup label="Offline"></MemberGroup> */}

View File

@ -2,26 +2,27 @@ import React from 'react'
import { Avatar, EthAddress, Flex, Text } from '~/src/system' import { Avatar, EthAddress, Flex, Text } from '~/src/system'
import type { Member } from '~/src/protocol'
import type { AvatarProps } from '~/src/system/avatar' import type { AvatarProps } from '~/src/system/avatar'
interface Props { interface Props {
children: string
chatKey: string
verified: boolean verified: boolean
untrustworthy: boolean untrustworthy: boolean
indicator?: AvatarProps['indicator'] indicator?: AvatarProps['indicator']
member: Member
} }
export const MemberItem = (props: Props) => { export const MemberItem = (props: Props) => {
const { children, chatKey, indicator, verified, untrustworthy } = props const { member, indicator, verified, untrustworthy } = props
const { publicKey, username, colorHash } = member
return ( return (
<Flex gap="2" align="center" css={{ height: 56 }}> <Flex gap="2" align="center" css={{ height: 56 }}>
<Avatar size={32} indicator={indicator} /> <Avatar size={32} indicator={indicator} colorHash={colorHash} />
<div> <div>
<Flex align="center" gap={1}> <Flex align="center" gap={1}>
<Text size="15" color="accent" truncate> <Text size="15" color="accent" truncate>
{children} {username}
</Text> </Text>
{verified && ( {verified && (
<svg <svg
@ -59,7 +60,7 @@ export const MemberItem = (props: Props) => {
)} )}
</Flex> </Flex>
<EthAddress size={10} color="gray"> <EthAddress size={10} color="gray">
{chatKey} {publicKey}
</EthAddress> </EthAddress>
</div> </div>
</Flex> </Flex>

View File

@ -3,4 +3,5 @@ export { useAccount } from './use-account'
export type { Chat } from './use-chat' export type { Chat } from './use-chat'
export { useChat } from './use-chat' export { useChat } from './use-chat'
export { useChats } from './use-chats' export { useChats } from './use-chats'
export type { Member } from './use-members'
export { useMembers } from './use-members' export { useMembers } from './use-members'

View File

@ -1,11 +1,12 @@
import { useProtocol } from '~/src/protocol' import { useProtocol } from '~/src/protocol'
import type { Community } from '@status-im/js' import type { Member } from '@status-im/js'
export type Member = Community['description']['members'][0] // todo: remove in favor of useCommunity
export const useMembers = (): Member[] => {
const { client } = useProtocol()
export const useMembers = (): string[] => { return client.community.members
const { community } = useProtocol()
return Object.keys(community.members)
} }
export type { Member }