diff --git a/ts/components/dialog/edit-profile/EditProfileDialog.tsx b/ts/components/dialog/edit-profile/EditProfileDialog.tsx index 25ace9726..0a83498e9 100644 --- a/ts/components/dialog/edit-profile/EditProfileDialog.tsx +++ b/ts/components/dialog/edit-profile/EditProfileDialog.tsx @@ -4,15 +4,13 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { Dispatch } from '@reduxjs/toolkit'; -import { SyncUtils, UserUtils } from '../../../session/utils'; +import { UserUtils } from '../../../session/utils'; import { YourSessionIDPill, YourSessionIDSelectable } from '../../basic/YourSessionIDPill'; import { useHotkey } from '../../../hooks/useHotkey'; import { useOurAvatarPath, useOurConversationUsername } from '../../../hooks/useParamSelector'; -import { ConversationTypeEnum } from '../../../models/conversationAttributes'; -import { getConversationController } from '../../../session/conversations'; +import { ProfileManager } from '../../../session/profile_manager/ProfileManager'; import { editProfileModal, updateEditProfilePictureModel } from '../../../state/ducks/modalDialog'; -import { setLastProfileUpdateTimestamp } from '../../../util/storage'; import { SessionWrapperModal } from '../../SessionWrapperModal'; import { Flex } from '../../basic/Flex'; import { SessionButton } from '../../basic/SessionButton'; @@ -166,20 +164,6 @@ const StyledSessionIdSection = styled(Flex)` } `; -const updateDisplayName = async (newName: string) => { - const ourNumber = UserUtils.getOurPubKeyStrFromCache(); - const conversation = await getConversationController().getOrCreateAndWait( - ourNumber, - ConversationTypeEnum.PRIVATE - ); - conversation.setSessionDisplayNameNoCommit(newName); - - // might be good to not trigger a sync if the name did not change - await conversation.commit(); - await setLastProfileUpdateTimestamp(Date.now()); - await SyncUtils.forceSyncConfigurationNowIfNeeded(true); -}; - export type ProfileDialogModes = 'default' | 'edit' | 'qr' | 'lightbox'; export const EditProfileDialog = () => { @@ -227,11 +211,21 @@ export const EditProfileDialog = () => { return; } - setLoading(true); - await updateDisplayName(profileName); - setUpdateProfileName(profileName); - setMode('default'); - setLoading(false); + try { + setLoading(true); + await ProfileManager.updateOurProfileDisplayName(profileName); + setUpdateProfileName(profileName); + setMode('default'); + } catch (err) { + // Note error substring is taken from libsession-util + if (err.message && err.message.includes('exceeds maximum length')) { + setProfileNameError(window.i18n('displayNameTooLong')); + } else { + setProfileNameError(window.i18n('unknownError')); + } + } finally { + setLoading(false); + } }; const handleProfileHeaderClick = () => { diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 255b27e6b..3e425c9dc 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -401,8 +401,8 @@ export class SwarmPolling { hash: m.hash, })); - await GenericWrapperActions.init('UserConfig', privateKeyEd25519, null); - await GenericWrapperActions.merge('UserConfig', incomingConfigMessages); + await UserConfigWrapperActions.init(privateKeyEd25519, null); + await UserConfigWrapperActions.merge(incomingConfigMessages); const userInfo = await UserConfigWrapperActions.getUserInfo(); if (!userInfo) { @@ -412,7 +412,7 @@ export class SwarmPolling { } catch (e) { window.log.warn('LibSessionUtil.initializeLibSessionUtilWrappers failed with', e.message); } finally { - await GenericWrapperActions.free('UserConfig'); + await UserConfigWrapperActions.free(); } return ''; diff --git a/ts/session/constants.ts b/ts/session/constants.ts index f7e882170..3f39a8a1d 100644 --- a/ts/session/constants.ts +++ b/ts/session/constants.ts @@ -79,9 +79,6 @@ export const VALIDATION = { export const DEFAULT_RECENT_REACTS = ['😂', '🥰', '😢', '😡', '😮', '😈']; export const REACT_LIMIT = 6; -/** character limit for a display name based on libsession MAX_NAME_LENGTH */ -export const MAX_NAME_LENGTH_BYTES = 100; - export const FEATURE_RELEASE_TIMESTAMPS = { DISAPPEARING_MESSAGES_V2: 1710284400000, // 13/03/2024 10:00 Melbourne time USER_CONFIG: 1690761600000, // Monday July 31st at 10am Melbourne time diff --git a/ts/session/profile_manager/ProfileManager.ts b/ts/session/profile_manager/ProfileManager.ts index eea852a0b..fd957f6c1 100644 --- a/ts/session/profile_manager/ProfileManager.ts +++ b/ts/session/profile_manager/ProfileManager.ts @@ -1,7 +1,10 @@ import { isEmpty } from 'lodash'; +import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../../models/conversationAttributes'; +import { setLastProfileUpdateTimestamp } from '../../util/storage'; +import { UserConfigWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface'; import { getConversationController } from '../conversations'; -import { UserUtils } from '../utils'; -import { toHex } from '../utils/String'; +import { SyncUtils, UserUtils } from '../utils'; +import { fromHexToArray, sanitizeSessionUsername, toHex } from '../utils/String'; import { AvatarDownload } from '../utils/job_runners/jobs/AvatarDownloadJob'; export type Profile = { @@ -92,7 +95,49 @@ async function updateProfileOfContact( } } +export async function updateOurProfileDisplayName(newName: string, onboarding?: boolean) { + const cleanName = sanitizeSessionUsername(newName).trim(); + + if (onboarding) { + await UserConfigWrapperActions.setUserInfo(cleanName, CONVERSATION_PRIORITIES.default, null); + return cleanName; + } + + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); + const conversation = await getConversationController().getOrCreateAndWait( + ourNumber, + ConversationTypeEnum.PRIVATE + ); + + const dbProfileUrl = conversation.get('avatarPointer'); + const dbProfileKey = conversation.get('profileKey') + ? fromHexToArray(conversation.get('profileKey')!) + : null; + const dbPriority = conversation.get('priority') || CONVERSATION_PRIORITIES.default; + + await UserConfigWrapperActions.setUserInfo( + cleanName, + dbPriority, + dbProfileUrl && dbProfileKey + ? { + url: dbProfileUrl, + key: dbProfileKey, + } + : null + ); + + conversation.setSessionDisplayNameNoCommit(newName); + + // might be good to not trigger a sync if the name did not change + await conversation.commit(); + await setLastProfileUpdateTimestamp(Date.now()); + await SyncUtils.forceSyncConfigurationNowIfNeeded(true); + + return cleanName; +} + export const ProfileManager = { updateOurProfileSync, updateProfileOfContact, + updateOurProfileDisplayName, }; diff --git a/ts/session/utils/String.ts b/ts/session/utils/String.ts index d44d63a76..9d80a34e4 100644 --- a/ts/session/utils/String.ts +++ b/ts/session/utils/String.ts @@ -1,5 +1,4 @@ import ByteBuffer from 'bytebuffer'; -import { MAX_NAME_LENGTH_BYTES } from '../constants'; export type Encoding = 'base64' | 'hex' | 'binary' | 'utf8'; export type BufferType = ByteBuffer | Buffer | ArrayBuffer | Uint8Array; @@ -56,19 +55,11 @@ const forbiddenDisplayCharRegex = /\uFFD2*/g; * This function removes any forbidden char from a given display name. * This does not trim it as otherwise, a user cannot type User A as when he hits the space, it gets trimmed right away. * The trimming should hence happen after calling this and on saving the display name. - * - * This function makes sure that the MAX_NAME_LENGTH_BYTES is verified for utf8 byte length. * @param inputName the input to sanitize * @returns a sanitized string, untrimmed */ export const sanitizeSessionUsername = (inputName: string) => { const validChars = inputName.replace(forbiddenDisplayCharRegex, ''); - - const lengthBytes = encode(validChars, 'utf8').byteLength; - if (lengthBytes > MAX_NAME_LENGTH_BYTES) { - throw new Error('Display name is too long'); - } - return validChars; }; diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index 63e2e0620..5b999550d 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -67,7 +67,7 @@ const generateKeypair = async ( * This registers a user account. It can also be used if an account restore fails and the user instead registers a new display name * @param mnemonic The mnemonic generated on first app loading and to use for this brand new user * @param mnemonicLanguage only 'english' is supported - * @param displayName the display name to register, character limit is MAX_NAME_LENGTH_BYTES + * @param displayName the display name to register * @param registerCallback when restoring an account, registration completion is handled elsewhere so we need to pass the pubkey back up to the caller */ export async function registerSingleDevice(