From 3e00330f5e4a1fa39f07e855d4ed32a0f2603b27 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Tue, 3 May 2022 13:12:48 +0200 Subject: [PATCH] feat: Include pinned safe apps in dashboard widget --- src/components/Dashboard/SafeApps/Grid.tsx | 4 +- .../Apps/__tests__/trackAppUsageCount.test.ts | 57 +++++++++++++++++-- .../components/Apps/trackAppUsageCount.ts | 48 +++++++++++----- 3 files changed, 88 insertions(+), 21 deletions(-) diff --git a/src/components/Dashboard/SafeApps/Grid.tsx b/src/components/Dashboard/SafeApps/Grid.tsx index 79a573ceb2..ff6c3bb577 100644 --- a/src/components/Dashboard/SafeApps/Grid.tsx +++ b/src/components/Dashboard/SafeApps/Grid.tsx @@ -12,7 +12,7 @@ import { GENERIC_APPS_ROUTE } from 'src/routes/routes' import Card, { CARD_HEIGHT, CARD_PADDING } from 'src/components/Dashboard/SafeApps/Card' import ExploreIcon from 'src/assets/icons/explore.svg' import { SafeApp } from 'src/routes/safe/components/Apps/types' -import { getAppsUsageData, rankTrackedSafeApps } from 'src/routes/safe/components/Apps/trackAppUsageCount' +import { getAppsUsageData, rankSafeApps } from 'src/routes/safe/components/Apps/trackAppUsageCount' import { FEATURED_APPS_TAG } from 'src/components/Dashboard/FeaturedApps/FeaturedApps' import { WidgetTitle, WidgetBody, WidgetContainer } from 'src/components/Dashboard/styled' @@ -61,7 +61,7 @@ const useRankedApps = (allApps: SafeApp[], pinnedSafeApps: SafeApp[], size: numb if (!allApps.length) return [] const trackData = getAppsUsageData() - const rankedSafeAppIds = rankTrackedSafeApps(trackData) + const rankedSafeAppIds = rankSafeApps(trackData, pinnedSafeApps) const featuredSafeAppIds = allApps.filter((app) => app.tags?.includes(FEATURED_APPS_TAG)).map((app) => app.id) const nonFeaturedApps = allApps.filter((app) => !featuredSafeAppIds.includes(app.id)) diff --git a/src/routes/safe/components/Apps/__tests__/trackAppUsageCount.test.ts b/src/routes/safe/components/Apps/__tests__/trackAppUsageCount.test.ts index dedcbc6f7f..06471701e4 100644 --- a/src/routes/safe/components/Apps/__tests__/trackAppUsageCount.test.ts +++ b/src/routes/safe/components/Apps/__tests__/trackAppUsageCount.test.ts @@ -1,4 +1,7 @@ -import { AppTrackData, rankTrackedSafeApps } from 'src/routes/safe/components/Apps/trackAppUsageCount' +import { AppTrackData, rankSafeApps } from 'src/routes/safe/components/Apps/trackAppUsageCount' +import { SafeApp } from 'src/routes/safe/components/Apps/types' +import { SafeAppAccessPolicyTypes } from '@gnosis.pm/safe-react-gateway-sdk' +import { FETCH_STATUS } from 'src/utils/requests' describe('rankTrackedSafeApps', () => { it('ranks more recent apps higher', () => { @@ -24,7 +27,7 @@ describe('rankTrackedSafeApps', () => { openCount: 1, }, } - const result = rankTrackedSafeApps(trackedSafeApps) + const result = rankSafeApps(trackedSafeApps, []) expect(result).toEqual(['3', '2', '4', '1']) }) @@ -42,7 +45,7 @@ describe('rankTrackedSafeApps', () => { }, '3': { timestamp: 8, - txCount: 3, + txCount: 4, openCount: 4, }, '4': { @@ -51,7 +54,53 @@ describe('rankTrackedSafeApps', () => { openCount: 2, }, } - const result = rankTrackedSafeApps(trackedSafeApps) + const result = rankSafeApps(trackedSafeApps, []) expect(result).toEqual(['3', '2', '4', '1']) }) + + it('includes pinned apps in ranking', () => { + const trackedSafeApps: AppTrackData = { + '1': { + timestamp: 1, + txCount: 1, + openCount: 1, + }, + '2': { + timestamp: 4, + txCount: 4, + openCount: 6, + }, + '3': { + timestamp: 8, + txCount: 3, + openCount: 4, + }, + '4': { + timestamp: 5, + txCount: 2, + openCount: 2, + }, + } + + const pinnedApps: SafeApp[] = [ + { + id: '5', + url: '', + name: '', + iconUrl: '', + description: '', + chainIds: ['1'], + provider: undefined, + accessControl: { + type: SafeAppAccessPolicyTypes.DomainAllowlist, + value: [], + }, + fetchStatus: FETCH_STATUS.SUCCESS, + tags: [], + }, + ] + + const result = rankSafeApps(trackedSafeApps, pinnedApps) + expect(result).toEqual(['5', '2', '3', '4', '1']) + }) }) diff --git a/src/routes/safe/components/Apps/trackAppUsageCount.ts b/src/routes/safe/components/Apps/trackAppUsageCount.ts index 372d90de24..ca582fb9bd 100644 --- a/src/routes/safe/components/Apps/trackAppUsageCount.ts +++ b/src/routes/safe/components/Apps/trackAppUsageCount.ts @@ -5,8 +5,7 @@ export const APPS_DASHBOARD = 'APPS_DASHBOARD' const TX_COUNT_WEIGHT = 2 const OPEN_COUNT_WEIGHT = 1 -const MORE_RECENT_MULTIPLIER = 2 -const LESS_RECENT_MULTIPLIER = 1 +const PINNED_WEIGHT = 20 export type AppTrackData = { [safeAppId: string]: { @@ -46,20 +45,39 @@ export const trackSafeAppTxCount = (id: SafeApp['id']): void => { }) } -export const rankTrackedSafeApps = (apps: AppTrackData): string[] => { - const appsMap = Object.entries(apps) +const normalizeBetweenTwoRanges = (val: number, minVal: number, maxVal: number, newMin: number, newMax: number) => { + return newMin + ((val - minVal) * (newMax - newMin)) / (maxVal - minVal) +} + +export const rankSafeApps = (apps: AppTrackData, pinnedSafeApps: SafeApp[]): string[] => { + const appsWithScore = computeTrackedSafeAppsScore(apps) + + for (const app of pinnedSafeApps) { + if (appsWithScore[app.id]) { + appsWithScore[app.id] += PINNED_WEIGHT + } else { + appsWithScore[app.id] = PINNED_WEIGHT + } + } + + return Object.entries(appsWithScore) + .sort((a, b) => b[1] - a[1]) + .map((app) => app[0]) +} + +export const computeTrackedSafeAppsScore = (apps: AppTrackData): Record => { + const scoredApps: Record = {} - return appsMap - .sort((a, b) => { - // The more recently used app gets a bigger score/relevancy multiplier - const aTimeMultiplier = a[1].timestamp - b[1].timestamp > 0 ? MORE_RECENT_MULTIPLIER : LESS_RECENT_MULTIPLIER - const bTimeMultiplier = b[1].timestamp - a[1].timestamp > 0 ? MORE_RECENT_MULTIPLIER : LESS_RECENT_MULTIPLIER + const sortedByTimestamp = Object.entries(apps).sort((a, b) => { + return a[1].timestamp - b[1].timestamp + }) - // The sorting score is a weighted function where the OPEN_COUNT weights differently than the TX_COUNT - const aScore = (TX_COUNT_WEIGHT * a[1].txCount + OPEN_COUNT_WEIGHT * a[1].openCount) * aTimeMultiplier - const bScore = (TX_COUNT_WEIGHT * b[1].txCount + OPEN_COUNT_WEIGHT * b[1].openCount) * bTimeMultiplier + for (const [idx, app] of sortedByTimestamp.entries()) { + // UNIX Timestamps add too much weight, so we normalize by uniformly distributing them to range [1..2] + const timeMultiplier = normalizeBetweenTwoRanges(idx, 0, sortedByTimestamp.length, 1, 2) + + scoredApps[app[0]] = (TX_COUNT_WEIGHT * app[1].txCount + OPEN_COUNT_WEIGHT * app[1].openCount) * timeMultiplier + } - return bScore - aScore - }) - .map((values) => values[0]) + return scoredApps }