-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Avatar changes when from offline to online #32090
Changes from 9 commits
bcb57a3
7e9768e
3d2086a
132199c
61c4ad0
a9d8b7c
533d90f
87fb8e7
0bac8d7
874fe34
f66f6a1
674d060
fe5a7d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,10 @@ | ||
import Str from 'expensify-common/lib/str'; | ||
import {SvgProps} from 'react-native-svg'; | ||
import {ValueOf} from 'type-fest'; | ||
import Onyx, { OnyxEntry } from 'react-native-onyx'; | ||
import _ from 'lodash'; | ||
import ONYXKEYS from '@src/ONYXKEYS'; | ||
import { PersonalDetails } from '@src/types/onyx'; | ||
import * as defaultAvatars from '@components/Icon/DefaultAvatars'; | ||
import {ConciergeAvatar, FallbackAvatar} from '@components/Icon/Expensicons'; | ||
import CONST from '@src/CONST'; | ||
|
@@ -13,6 +17,12 @@ type AvatarSource = React.FC<SvgProps> | string; | |
|
||
type LoginListIndicator = ValueOf<typeof CONST.BRICK_ROAD_INDICATOR_STATUS> | ''; | ||
|
||
let allPersonalDetails: OnyxEntry<Record<string, PersonalDetails>>; | ||
Onyx.connect({ | ||
key: ONYXKEYS.PERSONAL_DETAILS_LIST, | ||
callback: (val) => (allPersonalDetails = _.isEmpty(val) ? {} : val), | ||
}); | ||
|
||
/** | ||
* Searches through given loginList for any contact method / login with an error. | ||
* | ||
|
@@ -68,12 +78,19 @@ function hashText(text: string, range: number): number { | |
return Math.abs(hashCode(text.toLowerCase())) % range; | ||
} | ||
|
||
/** | ||
* Generate a random accountID base on searchValue. | ||
*/ | ||
function generateAccountID(searchValue: string): number { | ||
return hashText(searchValue, 2 ** 32); | ||
} | ||
|
||
/** | ||
* Helper method to return the default avatar associated with the given accountID | ||
* @param [accountID] | ||
* @returns | ||
*/ | ||
function getDefaultAvatar(accountID = -1): React.FC<SvgProps> { | ||
function getDefaultAvatar(accountID = -1, avatarURL?: string): React.FC<SvgProps> { | ||
if (accountID <= 0) { | ||
return FallbackAvatar; | ||
} | ||
|
@@ -83,21 +100,47 @@ function getDefaultAvatar(accountID = -1): React.FC<SvgProps> { | |
|
||
// There are 24 possible default avatars, so we choose which one this user has based | ||
// on a simple modulo operation of their login number. Note that Avatar count starts at 1. | ||
const accountIDHashBucket = ((accountID % CONST.DEFAULT_AVATAR_COUNT) + 1) as AvatarRange; | ||
|
||
// When we create a chat with a new user, an optimistic ID is used, and the avatar is generated based on it. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you make the comment more concise, it's a bit too long I think. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I have updated the comment. |
||
// When the response is received from the backend, the optimistic ID is replaced with the actual user ID. | ||
// However, the avatar link returned by the backend still matches the one generated using the optimistic ID. | ||
// Therefore, we cannot directly use the actual user ID to retrieve the SVG image number for the avatar. | ||
// Instead, we extract it from the avatar link returned by the backend. | ||
let accountIDHashBucket: AvatarRange; | ||
if (avatarURL) { | ||
const match = avatarURL.match(/(?<=default-avatar_)\d+(?=\.)/); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we have one more default avatar prefix |
||
const lastDigit = match && parseInt(match[0], 10); | ||
accountIDHashBucket = lastDigit as AvatarRange; | ||
} else { | ||
accountIDHashBucket = ((accountID % CONST.DEFAULT_AVATAR_COUNT) + 1) as AvatarRange; | ||
} | ||
return defaultAvatars[`Avatar${accountIDHashBucket}`]; | ||
} | ||
|
||
/** | ||
* Helper method to return default avatar URL associated with login | ||
*/ | ||
function getDefaultAvatarURL(accountID: string | number = '', isNewDot = false): string { | ||
function getDefaultAvatarURL(accountID: string | number = '', isNewDot = true): string { | ||
if (Number(accountID) === CONST.ACCOUNT_ID.CONCIERGE) { | ||
return CONST.CONCIERGE_ICON_URL; | ||
} | ||
|
||
// To ensure that the avatar remains unchanged and matches the one returned by the backend, | ||
// utilize an optimistic ID generated from the email instead of directly using the user ID. | ||
let email; let originAccountID; | ||
if (allPersonalDetails?.[accountID]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment also needed here to explain why this code is here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment added |
||
if (allPersonalDetails[accountID].login) { | ||
email = allPersonalDetails[accountID].login; | ||
} else if (allPersonalDetails[accountID].displayName) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder how useful this is, since the displayName could have changed and may no longer be an email? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I have already updated my PR. Thank you. |
||
email = allPersonalDetails[accountID].displayName; | ||
} | ||
} | ||
if (email) { | ||
originAccountID = generateAccountID(email); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I understand right, I think this would clearer if it was named There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I have updated it to originalOptimisticAccountID. |
||
} else { | ||
originAccountID = accountID; | ||
} | ||
// Note that Avatar count starts at 1 which is why 1 has to be added to the result (or else 0 would result in a broken avatar link) | ||
const accountIDHashBucket = (Number(accountID) % (isNewDot ? CONST.DEFAULT_AVATAR_COUNT : CONST.OLD_DEFAULT_AVATAR_COUNT)) + 1; | ||
const accountIDHashBucket = (Number(originAccountID) % (isNewDot ? CONST.DEFAULT_AVATAR_COUNT : CONST.OLD_DEFAULT_AVATAR_COUNT)) + 1; | ||
const avatarPrefix = isNewDot ? `default-avatar` : `avatar`; | ||
|
||
return `${CONST.CLOUDFRONT_URL}/images/avatars/${avatarPrefix}_${accountIDHashBucket}.png`; | ||
|
@@ -135,7 +178,7 @@ function isDefaultAvatar(avatarSource?: AvatarSource): boolean { | |
* @param accountID - the accountID of the user | ||
*/ | ||
function getAvatar(avatarSource: AvatarSource, accountID?: number): AvatarSource { | ||
return isDefaultAvatar(avatarSource) ? getDefaultAvatar(accountID) : avatarSource; | ||
return isDefaultAvatar(avatarSource) ? getDefaultAvatar(accountID, avatarSource as string) : avatarSource; | ||
} | ||
|
||
/** | ||
|
@@ -184,13 +227,6 @@ function getSmallSizeAvatar(avatarSource: AvatarSource, accountID?: number): Ava | |
return `${source.substring(0, lastPeriodIndex)}_128${source.substring(lastPeriodIndex)}`; | ||
} | ||
|
||
/** | ||
* Generate a random accountID base on searchValue. | ||
*/ | ||
function generateAccountID(searchValue: string): number { | ||
return hashText(searchValue, 2 ** 32); | ||
} | ||
|
||
/** | ||
* Gets the secondary phone login number | ||
*/ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should avoid this pattern.
ref: https://expensify.slack.com/archives/C01GTK53T8Q/p1688630003862989
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The link doesn't work for me for some reason 😅 Can you post some more detail here maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bump on this question
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Managed to get the link to load by pasting it into a chat with myself on Slack! 😆
As far as I can tell we aren't memoizing any of the calls to
getDefaultAvatarURL
so I don't think the issue is particularly relevant here. Many of the calls togetDefaultAvatarURL
are also fromReportUtils
which also uses this pattern so it wouldn't be a very useful optimization 😄There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Julesssss Friendly bump on the above - I think we should be good to merge!