From c07271109fa4eeeeca20aa5c26cf078d89b39c20 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 20 Apr 2021 15:52:19 +1000 Subject: [PATCH] Add a ConversationType OPEN_GROUP --- .../session/LeftPaneContactSection.tsx | 7 +++--- .../session/LeftPaneMessageSection.tsx | 7 +++--- .../usingClosedConversationDetails.tsx | 3 ++- ts/models/conversation.ts | 13 +++++++--- ts/models/message.ts | 7 ++++-- ts/opengroup/opengroupV1/OpenGroup.ts | 4 +-- .../opengroupV2/OpenGroupManagerV2.ts | 12 ++++++--- ts/receiver/closedGroups.ts | 8 +++--- ts/receiver/common.ts | 5 ---- ts/receiver/configMessage.ts | 3 ++- ts/receiver/contentMessage.ts | 3 ++- ts/receiver/dataMessage.ts | 14 +++++------ ts/receiver/errors.ts | 3 ++- ts/receiver/queuedJob.ts | 4 +-- ts/receiver/receiver.ts | 3 ++- ts/session/conversations/index.ts | 25 +++++++------------ ts/session/group/index.ts | 11 +++++--- ts/test/test-utils/utils/message.ts | 13 ++++++---- ts/util/accountManager.ts | 3 ++- 19 files changed, 82 insertions(+), 66 deletions(-) diff --git a/ts/components/session/LeftPaneContactSection.tsx b/ts/components/session/LeftPaneContactSection.tsx index aef6f6f11..72dbb8e18 100644 --- a/ts/components/session/LeftPaneContactSection.tsx +++ b/ts/components/session/LeftPaneContactSection.tsx @@ -8,7 +8,7 @@ import { SessionButtonType, } from './SessionButton'; import { AutoSizer, List } from 'react-virtualized'; -import { ConversationType } from '../../state/ducks/conversations'; +import { ConversationType as ReduxConversationType } from '../../state/ducks/conversations'; import { SessionClosableOverlay, SessionClosableOverlayType, @@ -18,9 +18,10 @@ import { DefaultTheme } from 'styled-components'; import { LeftPaneSectionHeader } from './LeftPaneSectionHeader'; import { ConversationController } from '../../session/conversations'; import { PubKey } from '../../session/types'; +import { ConversationType } from '../../models/conversation'; export interface Props { - directContacts: Array; + directContacts: Array; theme: DefaultTheme; openConversationExternal: (id: string, messageId?: string) => void; } @@ -128,7 +129,7 @@ export class LeftPaneContactSection extends React.Component { ToastUtils.pushToastError('addContact', error); } else { void ConversationController.getInstance() - .getOrCreateAndWait(sessionID, 'private') + .getOrCreateAndWait(sessionID, ConversationType.PRIVATE) .then(() => { this.props.openConversationExternal(sessionID); }); diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index fa94ae0d8..8feb3d359 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -7,7 +7,7 @@ import { ConversationListItemProps, ConversationListItemWithDetails, } from '../ConversationListItem'; -import { ConversationType } from '../../state/ducks/conversations'; +import { ConversationType as ReduxConversationType } from '../../state/ducks/conversations'; import { SearchResults, SearchResultsProps } from '../SearchResults'; import { SessionSearchInput } from './SessionSearchInput'; import { debounce } from 'lodash'; @@ -31,11 +31,12 @@ import { DefaultTheme } from 'styled-components'; import { LeftPaneSectionHeader } from './LeftPaneSectionHeader'; import { ConversationController } from '../../session/conversations'; import { OpenGroup } from '../../opengroup/opengroupV1/OpenGroup'; +import { ConversationType } from '../../models/conversation'; export interface Props { searchTerm: string; - contacts: Array; + contacts: Array; conversations?: Array; searchResults?: SearchResultsProps; @@ -395,7 +396,7 @@ export class LeftPaneMessageSection extends React.Component { if (!error) { await ConversationController.getInstance().getOrCreateAndWait( pubkey, - 'private' + ConversationType.PRIVATE ); openConversationExternal(pubkey); } else { diff --git a/ts/components/session/usingClosedConversationDetails.tsx b/ts/components/session/usingClosedConversationDetails.tsx index 0043a9fa2..ccdabc82d 100644 --- a/ts/components/session/usingClosedConversationDetails.tsx +++ b/ts/components/session/usingClosedConversationDetails.tsx @@ -3,6 +3,7 @@ import { PubKey } from '../../session/types'; import React from 'react'; import * as _ from 'lodash'; import { ConversationController } from '../../session/conversations'; +import { ConversationType } from '../../models/conversation'; export type ConversationAvatar = { avatarPath?: string; @@ -71,7 +72,7 @@ export function usingClosedConversationDetails(WrappedComponent: any) { members.map(async m => ConversationController.getInstance().getOrCreateAndWait( m.key, - 'private' + ConversationType.PRIVATE ) ) ); diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 620409b18..944ba00b7 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -1,6 +1,5 @@ import Backbone from 'backbone'; import _ from 'lodash'; -import { ConversationType } from '../receiver/common'; import { getMessageQueue } from '../session'; import { ConversationController } from '../session/conversations'; import { ClosedGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage'; @@ -42,6 +41,12 @@ import { GroupInvitationMessage } from '../session/messages/outgoing/visibleMess import { ReadReceiptMessage } from '../session/messages/outgoing/controlMessage/receipt/ReadReceiptMessage'; import { OpenGroup } from '../opengroup/opengroupV1/OpenGroup'; +export enum ConversationType { + GROUP = 'group', + OPEN_GROUP = 'opengroup', + PRIVATE = 'private', +} + export interface ConversationAttributes { profileName?: string; id: string; @@ -1239,7 +1244,7 @@ export class ConversationModel extends Backbone.Model { public async getProfile(id: string) { const c = await ConversationController.getInstance().getOrCreateAndWait( id, - 'private' + ConversationType.PRIVATE ); // We only need to update the profile as they are all stored inside the conversation @@ -1553,7 +1558,7 @@ export class ConversationModel extends Backbone.Model { } public isPrivate() { - return this.get('type') === 'private'; + return this.get('type') === ConversationType.PRIVATE; } public getAvatarPath() { @@ -1595,7 +1600,7 @@ export class ConversationModel extends Backbone.Model { const convo = await ConversationController.getInstance().getOrCreateAndWait( message.get('source'), - 'private' + ConversationType.PRIVATE ); const iconUrl = await convo.getNotificationIcon(); diff --git a/ts/models/message.ts b/ts/models/message.ts index ab2bad059..e18023a40 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -22,7 +22,7 @@ import { import autoBind from 'auto-bind'; import { saveMessage } from '../../ts/data/data'; -import { ConversationModel } from './conversation'; +import { ConversationModel, ConversationType } from './conversation'; import { actions as conversationActions } from '../state/ducks/conversations'; import { VisibleMessage } from '../session/messages/outgoing/visibleMessage/VisibleMessage'; import { buildSyncMessage } from '../session/utils/syncUtils'; @@ -974,7 +974,10 @@ export class MessageModel extends Backbone.Model { return null; } - return ConversationController.getInstance().getOrCreate(source, 'private'); + return ConversationController.getInstance().getOrCreate( + source, + ConversationType.PRIVATE + ); } public isOutgoing() { diff --git a/ts/opengroup/opengroupV1/OpenGroup.ts b/ts/opengroup/opengroupV1/OpenGroup.ts index 4461b6d92..8bb30e6ce 100644 --- a/ts/opengroup/opengroupV1/OpenGroup.ts +++ b/ts/opengroup/opengroupV1/OpenGroup.ts @@ -1,5 +1,5 @@ import { allowOnlyOneAtATime } from '../../../js/modules/loki_primitives'; -import { ConversationModel } from '../../models/conversation'; +import { ConversationModel, ConversationType } from '../../models/conversation'; import { ConversationController } from '../../session/conversations'; import { PromiseUtils } from '../../session/utils'; import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/syncUtils'; @@ -286,7 +286,7 @@ export class OpenGroup { // Create conversation const conversation = await ConversationController.getInstance().getOrCreateAndWait( conversationId, - 'group' + ConversationType.GROUP // keep a group for this one as this is an old open group ); // Convert conversation to a public one diff --git a/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts b/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts index a8646c20b..b03432e5a 100644 --- a/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts +++ b/ts/opengroup/opengroupV2/OpenGroupManagerV2.ts @@ -4,7 +4,7 @@ import { removeV2OpenGroupRoom, saveV2OpenGroupRoom, } from '../../data/opengroups'; -import { ConversationModel } from '../../models/conversation'; +import { ConversationModel, ConversationType } from '../../models/conversation'; import { ConversationController } from '../../session/conversations'; import { getOpenGroupV2ConversationId } from '../utils/OpenGroupUtils'; import { openGroupV2GetRoomInfo } from './OpenGroupAPIV2'; @@ -47,7 +47,7 @@ async function attemptConnectionV2( }); } - // the convo does not exist. Make sure the db is clean too + // here, the convo does not exist. Make sure the db is clean too await removeV2OpenGroupRoom(conversationId); const room: OpenGroupV2Room = { @@ -58,9 +58,15 @@ async function attemptConnectionV2( }; try { - // save the pubkey to the db. + // save the pubkey to the db, the request for room Info will need it and access it from the db await saveV2OpenGroupRoom(room); const info = await openGroupV2GetRoomInfo(roomId, serverUrl); + const conversation = await ConversationController.getInstance().getOrCreateAndWait( + conversationId, + ConversationType.OPEN_GROUP + ); + conversation.isPublic(); + console.warn('openGroupRoom info', info); } catch (e) { window.log.warn('Failed to join open group v2', e); diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index dee13db0d..fd631b5a7 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -26,7 +26,7 @@ import { import { ECKeyPair, HexKeyPair } from './keypairs'; import { UserUtils } from '../session/utils'; -import { ConversationModel } from '../models/conversation'; +import { ConversationModel, ConversationType } from '../models/conversation'; import _ from 'lodash'; import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils'; import { MessageController } from '../session/messages'; @@ -218,7 +218,7 @@ export async function handleNewClosedGroup( maybeConvo || (await ConversationController.getInstance().getOrCreateAndWait( groupId, - 'group' + ConversationType.GROUP )); // ***** Creating a new group ***** log.info('Received a new ClosedGroup of id:', groupId); @@ -847,7 +847,7 @@ async function sendLatestKeyPairToUsers( ); await ConversationController.getInstance().getOrCreateAndWait( member, - 'private' + ConversationType.PRIVATE ); const wrappers = await ClosedGroup.buildEncryptionKeyPairWrappers( @@ -917,7 +917,7 @@ export async function createClosedGroup( // Create the group const convo = await ConversationController.getInstance().getOrCreateAndWait( groupPublicKey, - 'group' + ConversationType.GROUP ); const admins = [ourNumber.key]; diff --git a/ts/receiver/common.ts b/ts/receiver/common.ts index 570b2ade4..5f7280c0b 100644 --- a/ts/receiver/common.ts +++ b/ts/receiver/common.ts @@ -10,8 +10,3 @@ export function getEnvelopeId(envelope: EnvelopePlus) { return envelope.id; } - -export enum ConversationType { - GROUP = 'group', - PRIVATE = 'private', -} diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index bbc59aa4f..c14e88c06 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -4,6 +4,7 @@ import { getItemById, hasSyncedInitialConfigurationItem, } from '../data/data'; +import { ConversationType } from '../models/conversation'; import { OpenGroup } from '../opengroup/opengroupV1/OpenGroup'; import { SignalService } from '../protobuf'; import { ConversationController } from '../session/conversations'; @@ -123,7 +124,7 @@ async function handleGroupsAndContactsFromConfigMessage( } const contactConvo = await ConversationController.getInstance().getOrCreateAndWait( toHex(c.publicKey), - 'private' + ConversationType.PRIVATE ); const profile = { displayName: c.name, diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 6bd95da22..aa78ce2aa 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -16,6 +16,7 @@ import { ECKeyPair } from './keypairs'; import { KeyPairRequestManager } from './keyPairRequestManager'; import { requestEncryptionKeyPair } from '../session/group'; import { handleConfigurationMessage } from './configMessage'; +import { ConversationType } from '../models/conversation'; export async function handleContentMessage(envelope: EnvelopePlus) { try { @@ -383,7 +384,7 @@ export async function innerHandleContentMessage( await ConversationController.getInstance().getOrCreateAndWait( envelope.source, - 'private' + ConversationType.PRIVATE ); if (content.dataMessage) { diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 4d161d749..5cbd90937 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -1,7 +1,7 @@ import { SignalService } from './../protobuf'; import { removeFromCache } from './cache'; import { EnvelopePlus } from './types'; -import { ConversationType, getEnvelopeId } from './common'; +import { getEnvelopeId } from './common'; import { PubKey } from '../session/types'; import { handleMessageJob } from './queuedJob'; @@ -14,7 +14,7 @@ import { handleClosedGroupControlMessage } from './closedGroups'; import { MessageModel } from '../models/message'; import { MessageModelType } from '../models/messageType'; import { getMessageBySender } from '../../ts/data/data'; -import { ConversationModel } from '../models/conversation'; +import { ConversationModel, ConversationType } from '../models/conversation'; import { DeliveryReceiptMessage } from '../session/messages/outgoing/controlMessage/receipt/DeliveryReceiptMessage'; export async function updateProfile( @@ -75,7 +75,7 @@ export async function updateProfile( const conv = await ConversationController.getInstance().getOrCreateAndWait( conversation.id, - 'private' + ConversationType.PRIVATE ); await conv.setLokiProfile(newProfile); } @@ -293,7 +293,7 @@ export async function handleDataMessage( const senderConversation = await ConversationController.getInstance().getOrCreateAndWait( senderPubKey, - 'private' + ConversationType.PRIVATE ); // Check if we need to update any profile names @@ -405,7 +405,7 @@ async function handleProfileUpdate( const ourNumber = UserUtils.getOurPubKeyStrFromCache(); const me = await ConversationController.getInstance().getOrCreate( ourNumber, - 'private' + ConversationType.PRIVATE ); // Will do the save for us if needed @@ -413,7 +413,7 @@ async function handleProfileUpdate( } else { const sender = await ConversationController.getInstance().getOrCreateAndWait( convoId, - 'private' + ConversationType.PRIVATE ); // Will do the save for us @@ -632,7 +632,7 @@ export async function handleMessageEvent(event: MessageEvent): Promise { const conversation = await ConversationController.getInstance().getOrCreateAndWait( conversationId, - isGroupMessage ? 'group' : 'private' + type ); if (!conversation) { diff --git a/ts/receiver/errors.ts b/ts/receiver/errors.ts index 40f157519..8e043e30a 100644 --- a/ts/receiver/errors.ts +++ b/ts/receiver/errors.ts @@ -3,6 +3,7 @@ import { toNumber } from 'lodash'; import { ConversationController } from '../session/conversations'; import { MessageController } from '../session/messages'; import { actions as conversationActions } from '../state/ducks/conversations'; +import { ConversationType } from '../models/conversation'; export async function onError(ev: any) { const { error } = ev; @@ -20,7 +21,7 @@ export async function onError(ev: any) { const id = message.get('conversationId'); const conversation = await ConversationController.getInstance().getOrCreateAndWait( id, - 'private' + ConversationType.PRIVATE ); // force conversation unread count to be > 0 so it is highlighted conversation.set({ diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index aa18c5123..ede6347fe 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import { SignalService } from '../protobuf'; import { StringUtils, UserUtils } from '../session/utils'; import { ConversationController } from '../session/conversations'; -import { ConversationModel } from '../models/conversation'; +import { ConversationModel, ConversationType } from '../models/conversation'; import { MessageCollection, MessageModel } from '../models/message'; import { MessageController } from '../session/messages'; import { getMessageById, getMessagesBySentAt } from '../../ts/data/data'; @@ -401,7 +401,7 @@ async function handleRegularMessage( const sendingDeviceConversation = await ConversationController.getInstance().getOrCreateAndWait( source, - 'private' + ConversationType.PRIVATE ); if (dataMessage.profileKey) { diff --git a/ts/receiver/receiver.ts b/ts/receiver/receiver.ts index 5542bef22..47fff475b 100644 --- a/ts/receiver/receiver.ts +++ b/ts/receiver/receiver.ts @@ -29,6 +29,7 @@ import { StringUtils, UserUtils } from '../session/utils'; import { SignalService } from '../protobuf'; import { ConversationController } from '../session/conversations'; import { removeUnprocessed } from '../data/data'; +import { ConversationType } from '../models/conversation'; // TODO: check if some of these exports no longer needed @@ -290,7 +291,7 @@ export async function handlePublicMessage(messageData: any) { if (!isMe && profile) { const conversation = await ConversationController.getInstance().getOrCreateAndWait( source, - 'private' + ConversationType.PRIVATE ); await updateProfile(conversation, profile, profileKey); } diff --git a/ts/session/conversations/index.ts b/ts/session/conversations/index.ts index 816d200c1..26ca8a03a 100644 --- a/ts/session/conversations/index.ts +++ b/ts/session/conversations/index.ts @@ -8,11 +8,11 @@ import { ConversationAttributes, ConversationCollection, ConversationModel, + ConversationType, } from '../../models/conversation'; import { BlockedNumberController } from '../../util'; import { getSnodesFor } from '../snode_api/snodePool'; import { PubKey } from '../types'; -import { UserUtils } from '../utils'; import { actions as conversationActions } from '../../state/ducks/conversations'; export class ConversationController { @@ -69,14 +69,18 @@ export class ConversationController { return this.conversations.add(attributes); } - public getOrCreate(id: string, type: string) { + public getOrCreate(id: string, type: ConversationType) { if (typeof id !== 'string') { throw new TypeError("'id' must be a string"); } - if (type !== 'private' && type !== 'group') { + if ( + type !== ConversationType.PRIVATE && + type !== ConversationType.GROUP && + type !== ConversationType.OPEN_GROUP + ) { throw new TypeError( - `'type' must be 'private' or 'group'; got: '${type}'` + `'type' must be 'private' or 'group' or 'opengroup; got: '${type}'` ); } @@ -98,17 +102,6 @@ export class ConversationController { } as any); const create = async () => { - if (!conversation.isValid()) { - const validationError = conversation.validationError || {}; - window.log.error( - 'Contact is not valid. Not saving, but adding to collection:', - conversation.idForLogging(), - validationError.stack - ); - - return conversation; - } - try { await saveConversation(conversation.attributes); } catch (error) { @@ -173,7 +166,7 @@ export class ConversationController { public async getOrCreateAndWait( id: string | PubKey, - type: 'private' | 'group' + type: ConversationType ): Promise { const initialPromise = this._initialPromise !== undefined diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index 8293d8dae..a3733f985 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -19,7 +19,7 @@ import { encryptUsingSessionProtocol } from '../crypto/MessageEncrypter'; import { ECKeyPair } from '../../receiver/keypairs'; import { UserUtils } from '../utils'; import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage'; -import { ConversationModel } from '../../models/conversation'; +import { ConversationModel, ConversationType } from '../../models/conversation'; import { MessageModel } from '../../models/message'; import { MessageModelType } from '../../models/messageType'; import { MessageController } from '../messages'; @@ -87,7 +87,7 @@ export async function initiateGroupUpdate( ) { const convo = await ConversationController.getInstance().getOrCreateAndWait( groupId, - 'group' + ConversationType.GROUP ); if (convo.isPublic()) { @@ -250,7 +250,7 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) { const conversation = await ConversationController.getInstance().getOrCreateAndWait( id, - 'group' + ConversationType.GROUP ); const updates: any = { @@ -450,7 +450,10 @@ async function sendAddedMembers( }); const promises = addedMembers.map(async m => { - await ConversationController.getInstance().getOrCreateAndWait(m, 'private'); + await ConversationController.getInstance().getOrCreateAndWait( + m, + ConversationType.PRIVATE + ); const memberPubKey = PubKey.cast(m); await getMessageQueue().sendToPubKey(memberPubKey, newClosedGroupUpdate); }); diff --git a/ts/test/test-utils/utils/message.ts b/ts/test/test-utils/utils/message.ts index 8114910a0..c7ee4b52c 100644 --- a/ts/test/test-utils/utils/message.ts +++ b/ts/test/test-utils/utils/message.ts @@ -1,7 +1,10 @@ import { v4 as uuid } from 'uuid'; import { generateFakePubKey, generateFakePubKeys } from './pubkey'; import { ClosedGroupVisibleMessage } from '../../../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage'; -import { ConversationAttributes } from '../../../models/conversation'; +import { + ConversationAttributes, + ConversationType, +} from '../../../models/conversation'; import { OpenGroupMessage } from '../../../session/messages/outgoing'; import { VisibleMessage } from '../../../session/messages/outgoing/visibleMessage/VisibleMessage'; import { OpenGroup } from '../../../opengroup/opengroupV1/OpenGroup'; @@ -49,13 +52,13 @@ export function generateClosedGroupMessage( interface MockConversationParams { id?: string; members?: Array; - type: 'private' | 'group' | 'public'; + type: ConversationType; isMediumGroup?: boolean; } export class MockConversation { public id: string; - public type: 'private' | 'group' | 'public'; + public type: ConversationType; public attributes: ConversationAttributes; constructor(params: MockConversationParams) { @@ -71,7 +74,7 @@ export class MockConversation { id: this.id, name: '', profileName: undefined, - type: params.type === 'public' ? 'group' : params.type, + type: params.type === ConversationType.OPEN_GROUP ? 'group' : params.type, members, left: false, expireTimer: 0, @@ -86,7 +89,7 @@ export class MockConversation { } public isPrivate() { - return this.type === 'private'; + return this.type === ConversationType.PRIVATE; } public isBlocked() { diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index 607b65152..cf7fe2724 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -18,6 +18,7 @@ import { import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils'; import { actions as userActions } from '../state/ducks/user'; import { mn_decode, mn_encode } from '../session/crypto/mnemonic'; +import { ConversationType } from '../models/conversation'; /** * Might throw @@ -238,7 +239,7 @@ async function registrationDone(ourPubkey: string, displayName: string) { // Ensure that we always have a conversation for ourself const conversation = await ConversationController.getInstance().getOrCreateAndWait( ourPubkey, - 'private' + ConversationType.PRIVATE ); await conversation.setLokiProfile({ displayName }); const user = {