Skip to content
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

[No QA][TS migration] Migrate 'UserUtils.js' lib to TypeScript #27778

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ type OnyxValues = {
[ONYXKEYS.COUNTRY_CODE]: number;
[ONYXKEYS.COUNTRY]: string;
[ONYXKEYS.USER]: OnyxTypes.User;
[ONYXKEYS.LOGIN_LIST]: OnyxTypes.Login;
[ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList;
blazejkustra marked this conversation as resolved.
Show resolved Hide resolved
[ONYXKEYS.SESSION]: OnyxTypes.Session;
[ONYXKEYS.BETAS]: OnyxTypes.Beta[];
[ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf<typeof CONST.PRIORITY_MODE>;
Expand Down
113 changes: 44 additions & 69 deletions src/libs/UserUtils.js → src/libs/UserUtils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import _ from 'underscore';
import lodashGet from 'lodash/get';
import {SvgProps} from 'react-native-svg';
import CONST from '../CONST';
import hashCode from './hashCode';
import * as Expensicons from '../components/Icon/Expensicons';
import {ConciergeAvatar, FallbackAvatar} from '../components/Icon/Expensicons';
import * as defaultAvatars from '../components/Icon/DefaultAvatars';
import LoginList from '../types/onyx/LoginList';

type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24;

/**
* Searches through given loginList for any contact method / login with an error.
Expand All @@ -25,36 +27,29 @@ import * as defaultAvatars from '../components/Icon/DefaultAvatars';
* }
* }
* }}
*
* @param {Object} loginList
* @param {Object} loginList.errorFields
* @returns {Boolean}
*/
function hasLoginListError(loginList) {
return _.some(loginList, (login) => _.some(lodashGet(login, 'errorFields', {}), (field) => !_.isEmpty(field)));
function hasLoginListError(loginList: LoginList): boolean {
const errorFields = loginList?.errorFields ?? {};
return Object.values(errorFields).some((field) => Object.keys(field).length > 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the type loginList: LoginList correct? if it is, we wouldn't need the question mark in loginList?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's correct, the thing is that this function is still used in some JS files and wrong values are passed and tests were failing. Once we get to these files that use this function the underlying issue will be fixed.

Either way, it's okay to make code safe and use nullish coalescing and optional chaining even if types says it's not necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either way, it's okay to make code safe and use nullish coalescing and optional chaining even if types says it's not necessary.

I disagree with this, I expect to be able to trust our types, but I understand that this may be acceptable while in a transitional period

I'm not sure why we didn't prefer loginList: Login | undefined | null if we had cases where we are passing undefined or null 🤷

}

/**
* Searches through given loginList for any contact method / login that requires
* an Info brick road status indicator. Currently this only applies if the user
* has an unvalidated contact method.
*
* @param {Object} loginList
* @param {String} loginList.validatedDate
* @returns {Boolean}
*/
function hasLoginListInfo(loginList) {
return _.some(loginList, (login) => _.isEmpty(login.validatedDate));
function hasLoginListInfo(loginList: LoginList): boolean {
return !loginList.validatedDate;
}

/**
* Gets the appropriate brick road indicator status for a given loginList.
* Error status is higher priority, so we check for that first.
*
* @param {Object} loginList
* @returns {String}
* @param loginList
* @returns
*/
function getLoginListBrickRoadIndicator(loginList) {
function getLoginListBrickRoadIndicator(loginList: LoginList): '' | 'error' | 'info' {
if (hasLoginListError(loginList)) {
blazejkustra marked this conversation as resolved.
Show resolved Hide resolved
return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
}
Expand All @@ -66,42 +61,35 @@ function getLoginListBrickRoadIndicator(loginList) {

/**
* Hashes provided string and returns a value between [0, range)
* @param {String} text
* @param {Number} range
* @returns {Number}
*/
function hashText(text, range) {
function hashText(text: string, range: number): number {
return Math.abs(hashCode(text.toLowerCase())) % range;
}

/**
* Helper method to return the default avatar associated with the given accountID
* @param {Number} [accountID]
* @returns {String}
* @param [accountID]
* @returns
*/
function getDefaultAvatar(accountID = -1) {
function getDefaultAvatar(accountID = -1): React.FC<SvgProps> {
if (accountID <= 0) {
return Expensicons.FallbackAvatar;
return FallbackAvatar;
}
if (Number(accountID) === CONST.ACCOUNT_ID.CONCIERGE) {
return Expensicons.ConciergeAvatar;
return ConciergeAvatar;
}

// 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;
const accountIDHashBucket = ((accountID % CONST.DEFAULT_AVATAR_COUNT) + 1) as AvatarRange;

return defaultAvatars[`Avatar${accountIDHashBucket}`];
}

/**
* Helper method to return default avatar URL associated with login
*
* @param {Number} [accountID]
* @param {Boolean} [isNewDot]
* @returns {String}
*/
function getDefaultAvatarURL(accountID = '', isNewDot = false) {
function getDefaultAvatarURL(accountID: string | number = '', isNewDot = false): string {
if (Number(accountID) === CONST.ACCOUNT_ID.CONCIERGE) {
return CONST.CONCIERGE_ICON_URL;
}
Expand All @@ -115,64 +103,57 @@ function getDefaultAvatarURL(accountID = '', isNewDot = false) {

/**
* Given a user's avatar path, returns true if user doesn't have an avatar or if URL points to a default avatar
* @param {String} [avatarURL] - the avatar source from user's personalDetails
* @returns {Boolean}
* @param [avatarURL] - the avatar source from user's personalDetails
*/
function isDefaultAvatar(avatarURL) {
if (
_.isString(avatarURL) &&
(avatarURL.includes('images/avatars/avatar_') || avatarURL.includes('images/avatars/default-avatar_') || avatarURL.includes('images/avatars/user/default'))
) {
return true;
}

// We use a hardcoded "default" Concierge avatar
if (_.isString(avatarURL) && (avatarURL === CONST.CONCIERGE_ICON_URL_2021 || avatarURL === CONST.CONCIERGE_ICON_URL)) {
return true;
function isDefaultAvatar(avatarURL?: string): boolean {
if (typeof avatarURL === 'string') {
if (avatarURL.includes('images/avatars/avatar_') || avatarURL.includes('images/avatars/default-avatar_') || avatarURL.includes('images/avatars/user/default')) {
return true;
}

// We use a hardcoded "default" Concierge avatar
if (avatarURL === CONST.CONCIERGE_ICON_URL_2021 || avatarURL === CONST.CONCIERGE_ICON_URL) {
return true;
}
}

if (!avatarURL) {
// If null URL, we should also use a default avatar
return true;
}

return false;
}

/**
* Provided a source URL, if source is a default avatar, return the associated SVG.
* Otherwise, return the URL pointing to a user-uploaded avatar.
*
* @param {String} avatarURL - the avatar source from user's personalDetails
* @param {Number} accountID - the accountID of the user
* @returns {String|Function}
* @param avatarURL - the avatar source from user's personalDetails
* @param accountID - the accountID of the user
*/
function getAvatar(avatarURL, accountID) {
function getAvatar(avatarURL: string, accountID: number): React.FC<SvgProps> | string {
return isDefaultAvatar(avatarURL) ? getDefaultAvatar(accountID) : avatarURL;
}

/**
* Provided an avatar URL, if avatar is a default avatar, return NewDot default avatar URL.
* Otherwise, return the URL pointing to a user-uploaded avatar.
*
* @param {String} avatarURL - the avatar source from user's personalDetails
* @param {Number} accountID - the accountID of the user
* @returns {String}
* @param avatarURL - the avatar source from user's personalDetails
* @param accountID - the accountID of the user
*/
function getAvatarUrl(avatarURL, accountID) {
function getAvatarUrl(avatarURL: string, accountID: number): string {
return isDefaultAvatar(avatarURL) ? getDefaultAvatarURL(accountID, true) : avatarURL;
}

/**
* Avatars uploaded by users will have a _128 appended so that the asset server returns a small version.
* This removes that part of the URL so the full version of the image can load.
*
* @param {String} [avatarURL]
* @param {Number} [accountID]
* @returns {String|Function}
*/
function getFullSizeAvatar(avatarURL, accountID) {
function getFullSizeAvatar(avatarURL: string, accountID: number): React.FC<SvgProps> | string {
const source = getAvatar(avatarURL, accountID);
if (!_.isString(source)) {
if (typeof source !== 'string') {
return source;
aldo-expensify marked this conversation as resolved.
Show resolved Hide resolved
}
return source.replace('_128', '');
Expand All @@ -181,14 +162,10 @@ function getFullSizeAvatar(avatarURL, accountID) {
/**
* Small sized avatars end with _128.<file-type>. This adds the _128 at the end of the
* source URL (before the file type) if it doesn't exist there already.
*
* @param {String} avatarURL
* @param {Number} accountID
* @returns {String|Function}
*/
function getSmallSizeAvatar(avatarURL, accountID) {
function getSmallSizeAvatar(avatarURL: string, accountID: number): React.FC<SvgProps> | string {
const source = getAvatar(avatarURL, accountID);
if (!_.isString(source)) {
if (typeof source !== 'string') {
return source;
}

Expand All @@ -207,10 +184,8 @@ function getSmallSizeAvatar(avatarURL, accountID) {

/**
* Generate a random accountID base on searchValue.
* @param {String} searchValue
* @returns {Number}
*/
function generateAccountID(searchValue) {
function generateAccountID(searchValue: string): number {
return hashText(searchValue, 2 ** 32);
}

Expand Down
4 changes: 2 additions & 2 deletions src/types/onyx/Login.ts → src/types/onyx/LoginList.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as OnyxCommon from './OnyxCommon';

type Login = {
type LoginList = {
/** Phone/Email associated with user */
partnerUserID?: string;

Expand All @@ -17,4 +17,4 @@ type Login = {
pendingFields?: OnyxCommon.ErrorFields;
};

export default Login;
export default LoginList;
3 changes: 2 additions & 1 deletion src/types/onyx/Response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import {OnyxUpdate} from 'react-native-onyx';
type Response = {
previousUpdateID?: number | string;
lastUpdateID?: number | string;
jsonCode?: number;
jsonCode?: number | string;
onyxData?: OnyxUpdate[];
requestID?: string;
message?: string;
};

export default Response;
4 changes: 2 additions & 2 deletions src/types/onyx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Task from './Task';
import Currency from './Currency';
import ScreenShareRequest from './ScreenShareRequest';
import User from './User';
import Login from './Login';
import LoginList from './LoginList';
import Session from './Session';
import Beta from './Beta';
import BlockedFromConcierge from './BlockedFromConcierge';
Expand Down Expand Up @@ -60,7 +60,7 @@ export type {
Currency,
ScreenShareRequest,
User,
Login,
LoginList,
Session,
Beta,
BlockedFromConcierge,
Expand Down
Loading