Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

feat: Include pinned safe apps in dashboard widget #3849

Merged
merged 2 commits into from
May 4, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions src/components/Dashboard/SafeApps/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -24,7 +27,7 @@ describe('rankTrackedSafeApps', () => {
openCount: 1,
},
}
const result = rankTrackedSafeApps(trackedSafeApps)
const result = rankSafeApps(trackedSafeApps, [])
expect(result).toEqual(['3', '2', '4', '1'])
})

Expand All @@ -42,7 +45,7 @@ describe('rankTrackedSafeApps', () => {
},
'3': {
timestamp: 8,
txCount: 3,
txCount: 4,
openCount: 4,
},
'4': {
Expand All @@ -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(['2', '3', '5', '4', '1'])
})
})
55 changes: 40 additions & 15 deletions src/routes/safe/components/Apps/trackAppUsageCount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 10

export type AppTrackData = {
[safeAppId: string]: {
Expand Down Expand Up @@ -46,20 +45,46 @@ export const trackSafeAppTxCount = (id: SafeApp['id']): void => {
})
}

export const rankTrackedSafeApps = (apps: AppTrackData): string[] => {
const appsMap = Object.entries(apps)
// https://stackoverflow.com/a/55212064
const normalizeBetweenTwoRanges = (
val: number,
minVal: number,
maxVal: number,
newMin: number,
newMax: number,
): 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])
}
iamacook marked this conversation as resolved.
Show resolved Hide resolved

export const computeTrackedSafeAppsScore = (apps: AppTrackData): Record<string, number> => {
const scoredApps: Record<string, number> = {}

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
}