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 b62bc86dfe
commit c9bffe4a52
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
11 changed files with 137 additions and 24 deletions

View File

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

View File

@ -2,10 +2,12 @@ import { waku_message } from 'js-waku'
import { MessageType } from '~/protos/enums'
import { getDifferenceByKeys } from '~/src/helpers/get-difference-by-keys'
import { getObjectsDifference } from '~/src/helpers/get-objects-difference'
import { idToContentTopic } from '../../contentTopic'
import { createSymKeyFromPassword } from '../../encryption'
import { Chat } from '../chat'
import { Member } from '../member'
import type { Client } from '../../client'
import type {
@ -21,6 +23,7 @@ export class Community {
private symmetricKey!: Uint8Array
public description!: CommunityDescription
public chats: Map<string, Chat>
#members: Map<string, Member>
public callback: ((description: CommunityDescription) => void) | undefined
constructor(client: Client, publicKey: string) {
@ -28,6 +31,7 @@ export class Community {
this.publicKey = publicKey
this.chats = new Map()
this.#members = new Map()
}
public async start() {
@ -52,6 +56,7 @@ export class Community {
this.description = description
this.observe()
this.addMembers(this.description.members)
// Chats
await this.observeChatMessages(this.description.chats)
@ -62,6 +67,18 @@ export class Community {
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 () => {
await this.client.waku.store.queryHistory([this.contentTopic], {
// 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) => {
if (this.description) {
// already handled
@ -161,6 +191,23 @@ export class Community {
if (Object.keys(addedChats).length) {
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

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 type { ChatMessage as Message } from './client/chat'
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'
type ColorHash = number[][]
export type ColorHash = number[][]
const COLOR_HASH_COLORS_COUNT = 32
const COLOR_HASH_SEGMENT_MAX_LENGTH = 5

View File

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

View File

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

View File

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

View File

@ -1,11 +1,12 @@
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[] => {
const { community } = useProtocol()
return Object.keys(community.members)
return client.community.members
}
export type { Member }