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

feat: add notification components #2514

Merged
merged 8 commits into from
May 21, 2024
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
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script lang="ts">
import { LogoName } from '@auxiliary/logo'
import { Button, IconButton, IconName, Pill } from '@bloomwalletio/ui'
import { IconButton, IconName } from '@bloomwalletio/ui'
import { ProfileActionsMenu, SidebarTab } from '@components'
import { APP_STAGE, AppStage } from '@core/app'
import { localize } from '@core/i18n'
import { SupportedNetworkId, SupportedStardustNetworkId, getEvmNetwork } from '@core/network'
import { SupportedStardustNetworkId } from '@core/network'
import { activeProfile, isSoftwareProfile } from '@core/profile/stores'
import {
DashboardRoute,
Expand All @@ -22,11 +22,6 @@
import LedgerStatusTile from './LedgerStatusTile.svelte'
import StrongholdStatusTile from './StrongholdStatusTile.svelte'
import { BackupToast, VersionToast } from './toasts'
import { selectedAccount } from '@core/account/stores'
import { checkActiveProfileAuth } from '@core/profile/actions'
import { LedgerAppName } from '@core/ledger/enums'
import { handleError } from '@core/error/handlers'
import { notificationsManager } from '@auxiliary/wallet-connect/notifications/classes'

let expanded = true
function toggleExpand(): void {
Expand Down Expand Up @@ -139,20 +134,6 @@
$governanceRouter.reset()
$settingsRouter.reset()
}

async function enableNotifications(): Promise<void> {
try {
await checkActiveProfileAuth(LedgerAppName.Ethereum)
} catch (error) {
return
}

try {
notificationsManager.registerAccount($selectedAccount, getEvmNetwork(SupportedNetworkId.Ethereum))
} catch (err) {
handleError(err)
}
}
</script>

<aside class:expanded class="flex flex-col justify-between">
Expand Down Expand Up @@ -186,12 +167,6 @@

{#if expanded}
<dashboard-sidebar-tiles class="w-full flex flex-col space-y-2">
{#if notificationsManager?.isRegistered($selectedAccount, getEvmNetwork(SupportedNetworkId.Ethereum))}
<Pill color="success">Can subscribe</Pill>
{:else}
<Button text="Enable notifications" on:click={() => enableNotifications()} />
{/if}

{#if APP_STAGE === AppStage.PROD}
<BackupToast />
{:else}
Expand Down
4 changes: 4 additions & 0 deletions packages/desktop/views/dashboard/components/Navbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import features from '@features/features'
import { DashboardDrawerRoute } from '../drawers'
import Breadcrumbs from './Breadcrumbs.svelte'
import NotificationsButton from './NotificationsButton.svelte'
</script>

<NavbarContainer draggable={IS_MAC}>
Expand Down Expand Up @@ -41,6 +42,9 @@
size="sm"
/>
{/if}
{#if features?.walletConnect?.notifications?.enabled}
<NotificationsButton />
{/if}
</div>
</div>
</NavbarContainer>
Expand Down
102 changes: 102 additions & 0 deletions packages/desktop/views/dashboard/components/NotificationsButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<script lang="ts">
import { notificationsManager } from '@auxiliary/wallet-connect/notifications'
import { Button, IconButton, IconName, Indicator, Popover, Tabs, Text } from '@bloomwalletio/ui'
import { selectedAccount } from '@core/account/stores'
import { handleError } from '@core/error/handlers'
import { localize } from '@core/i18n'
import { LedgerAppName } from '@core/ledger/enums'
import { SupportedNetworkId, getEvmNetwork } from '@core/network'
import { checkActiveProfileAuth } from '@core/profile/actions'
import { activeAccounts } from '@core/profile/stores'
import { NotificationTile } from '@ui'

const TABS = [
{ key: 'all', value: 'All' },
{ key: 'unread', value: 'Unread' },
]

let selectedTab = TABS[0]

const MAX_AMOUNT_OF_NOTIFICATIONS = 7
const evmNetwork = getEvmNetwork(SupportedNetworkId.Ethereum)
const notifications = notificationsManager.notificationsPerSubscription
$: notificationsToDisplay = Object.keys($notifications)
.flatMap((subscriptionTopic) =>
$notifications[subscriptionTopic].map((notification) => ({
...notification,
subscriptionTopic,
}))
)
.sort((a, b) => b.sentAt - a.sentAt)
.slice(0, MAX_AMOUNT_OF_NOTIFICATIONS)

let anchor: HTMLElement | undefined = undefined

$: isAtLeast1AccountRegistered =
evmNetwork && $activeAccounts.some((account) => notificationsManager.isRegistered(account, evmNetwork))

async function enableNotifications(): Promise<void> {
try {
await checkActiveProfileAuth(LedgerAppName.Ethereum)
} catch (error) {
return
}

try {
if ($selectedAccount && evmNetwork) {
notificationsManager.registerAccount($selectedAccount, evmNetwork)
}
} catch (err) {
handleError(err)
}
}
</script>

<button bind:this={anchor} class="relative flex items-center">
<IconButton
icon={IconName.Bell}
tooltip={localize('views.dashboard.dappNotifications.title')}
textColor="primary"
size="sm"
/>
{#if notificationsToDisplay.some((notification) => !notification.isRead)}
<Indicator
size="sm"
class="absolute top-0 right-0 box-content rounded-full
border-2 border-solid border-surface dark:border-surface-dark"
/>
{/if}
</button>

<Popover {anchor} event="click" placement="bottom-start" preventClose>
<div
class="flex flex-col justify-center items-center border border-solid border-stroke dark:border-stroke-dark rounded-xl w-80
shadow-lg overflow-hidden divide-y divide-solid divide-stroke dark:divide-stroke-dark bg-surface dark:bg-surface-dark"
>
{#if notificationsToDisplay.length}
<div class="w-full p-4">
<Tabs bind:selectedTab tabs={TABS} />
</div>
{#each notificationsToDisplay as notification}
<NotificationTile {notification} subscriptionTopic={notification.subscriptionTopic} />
{/each}
{#if Object.values($notifications).flat().length > MAX_AMOUNT_OF_NOTIFICATIONS}
<div class="p-3 w-full">
<Button size="xs" text={localize('views.dashboard.dappNotifications.viewAll')} width="full" />
</div>
{/if}
{:else if !isAtLeast1AccountRegistered}
<div class="px-3 py-8 w-full flex flex-col gap-4 items-center">
<Text type="body2" align="center">{localize('views.dashboard.dappNotifications.notEnabledHint')}</Text>
<Button
text={localize('views.dashboard.dappNotifications.enable')}
on:click={() => enableNotifications()}
/>
</div>
{:else}
<div class="px-3 py-8 w-full">
<Text type="body2" align="center">{localize('views.dashboard.dappNotifications.empty')}</Text>
</div>
{/if}
</div>
</Popover>
66 changes: 66 additions & 0 deletions packages/shared/src/components/avatars/NotificationAvatar.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script lang="ts">
import { Avatar, IconName, Tooltip } from '@bloomwalletio/ui'
import { darkMode } from '@core/app/stores'
import type { NotifyClientTypes } from '@walletconnect/notify-client'

export let subscription: NotifyClientTypes.NotifySubscription | undefined = undefined
export let notificationType: string | undefined = undefined

let dappImageError = false
let notificationImageError = false
$: hasDappImage = subscription?.metadata.icons[0] && !dappImageError

let dappAnchor: HTMLElement
let notificationAnchor: HTMLElement
</script>

<div class="relative self-start">
<div bind:this={dappAnchor}>
<Avatar
size="lg"
icon={!hasDappImage ? IconName.Application : undefined}
textColor="primary"
backgroundColor={$darkMode ? 'surface-2-dark' : 'surface-2'}
>
{#if hasDappImage}
<img
src={subscription?.metadata.icons[0]}
alt={subscription?.metadata?.name}
class="size-full"
on:error={() => (dappImageError = true)}
/>
{/if}
</Avatar>
</div>
{#if subscription?.metadata.name}
<Tooltip anchor={dappAnchor} text={subscription.metadata.name} placement="right" event="hover" />
{/if}
{#if notificationType && subscription?.scope[notificationType]}
<span
class="absolute -right-1 -bottom-1 bg-surface dark:bg-surface-dark p-0.5 rounded-full"
bind:this={notificationAnchor}
>
<Avatar
size="xs"
icon={notificationImageError ? IconName.Bell : undefined}
textColor="primary"
backgroundColor={$darkMode ? 'surface-2-dark' : 'surface-2'}
>
{#if !notificationImageError}
<img
src={subscription.scope[notificationType]?.imageUrls.sm}
alt={notificationType}
class="size-full"
on:error={() => (notificationImageError = true)}
/>
{/if}
</Avatar>
</span>
<Tooltip
anchor={notificationAnchor}
text={subscription.scope[notificationType].name}
placement="right"
event="hover"
/>
{/if}
</div>
1 change: 1 addition & 0 deletions packages/shared/src/components/avatars/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { default as GovernanceAvatar } from './GovernanceAvatar.svelte'
export { default as NftAvatar } from './NftAvatar.svelte'
export { default as NetworkAvatar } from './NetworkAvatar.svelte'
export { default as NetworkAvatarGroup } from './NetworkAvatarGroup.svelte'
export { default as NotificationAvatar } from './NotificationAvatar.svelte'
export { default as ProfileAvatar } from './ProfileAvatar.svelte'
export { default as ProfileAvatarWithBadge } from './ProfileAvatarWithBadge.svelte'
export { default as TokenAvatar } from './TokenAvatar.svelte'
27 changes: 27 additions & 0 deletions packages/shared/src/components/tiles/NotificationTile.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script lang="ts">
import { notificationsManager } from '@auxiliary/wallet-connect/notifications'
import { Text, Tile } from '@bloomwalletio/ui'
import { getBestTimeDuration } from '@core/utils'
import { NotificationAvatar } from '@ui/avatars'
import { NotifyClientTypes } from '@walletconnect/notify-client'

export let notification: NotifyClientTypes.NotifyNotification
export let subscriptionTopic: string

$: subscription = notificationsManager.getSubscriptionsForTopic(subscriptionTopic)
</script>

<Tile class="!rounded-none">
<div class="flex justify-between gap-4 w-full">
<NotificationAvatar {subscription} notificationType={notification.type} />
<div class="flex-grow flex flex-col items-start">
<div class="flex justify-between items-center gap-2">
<Text type="sm" lineClamp={1}>{notification.title}</Text>
<Text type="xs" fontWeight="normal"
>{getBestTimeDuration(new Date().getTime() - notification.sentAt, 'day', true)}</Text
>
</div>
<Text type="xs" fontWeight="normal" lineClamp={2}>{notification.body}</Text>
</div>
</div>
</Tile>
1 change: 1 addition & 0 deletions packages/shared/src/components/tiles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { default as AliasTile } from './AliasTile.svelte'
export { default as TokenAmountTile } from './TokenAmountTile.svelte'
export { default as TokenAvailableBalanceTile } from './TokenAvailableBalanceTile.svelte'
export { default as ClickableTile } from './ClickableTile.svelte'
export { default as NotificationTile} from './NotificationTile.svelte'
export { default as NftTile } from './NftTile.svelte'
export { default as ShimmerClaimingAccountTile } from './ShimmerClaimingAccountTile.svelte'
export { default as Tile } from './Tile.svelte'
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IAccountState } from '@core/account'
import { EvmNetworkId, IEvmNetwork } from '@core/network'
import { signMessage } from '@core/wallet'
import { NotifyClient, NotifyClientTypes } from '@walletconnect/notify-client'
import { Writable, writable } from 'svelte/store'
import { Writable, get, writable } from 'svelte/store'
import { NotifyEvent } from '../enums'
import { WALLET_CONNECT_CORE } from '../../constants/wallet-connect-core.constant'

Expand Down Expand Up @@ -136,6 +136,12 @@ export class NotificationsManager {
await this.addTrackedNetworkAccounts(accounts, networkId)
}

getSubscriptionsForTopic(topic: string): NotifyClientTypes.NotifySubscription | undefined {
return Object.values(get(this.subscriptionsPerAddress))
.map((subscriptions) => subscriptions[topic])
.find(Boolean)
}

async addTrackedNetworkAccounts(accounts: IAccountState[], networkId: EvmNetworkId): Promise<void> {
const newNetworkAddressesToTrack = new Set(
accounts
Expand Down
7 changes: 7 additions & 0 deletions packages/shared/src/lib/core/utils/tests/time.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ describe('File: time.ts', () => {
expect(getBestTimeDuration(MILLISECONDS_PER_SECOND * 30)).toEqual('30 seconds')
expect(getBestTimeDuration(MILLISECONDS_PER_SECOND)).toEqual('1 second')
})

it('should return best duration with minimal param', () => {
expect(getBestTimeDuration(ONE_DAY_MILLIS * 2, 'day', true)).toEqual('2d')
expect(getBestTimeDuration(ONE_MINUTE_MILLIS * 60, 'day', true)).toEqual('1h')
expect(getBestTimeDuration(ONE_MINUTE_MILLIS * 30, 'day', true)).toEqual('30m')
expect(getBestTimeDuration(MILLISECONDS_PER_SECOND, 'day', true)).toEqual('1s')
})
})

describe('Function: isFutureDateTime', () => {
Expand Down
14 changes: 6 additions & 8 deletions packages/shared/src/lib/core/utils/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,22 @@ export function isFutureDateTime(dateTime: Date): boolean {
*
* @returns {string}
*/
export const getBestTimeDuration = (millis: number, noDurationUnit: Duration = 'day'): string => {
const zeroTime = localize(`times.${noDurationUnit || 'day'}`, { values: { time: 0 } })
export const getBestTimeDuration = (millis: number, noDurationUnit: Duration = 'day', minimal = false): string => {
const zeroTime = minimal ? '0' : localize(`times.${noDurationUnit || 'day'}`, { values: { time: 0 } })

if (Number.isNaN(millis)) return zeroTime

const inDays = millis / (HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND)
if (inDays >= 1) return localize('times.day', { values: { time: inDays > 1 ? Math.ceil(inDays) : inDays } })
if (inDays >= 1) return minimal ? `${Math.ceil(inDays)}d` : localize('times.day', { values: { time: inDays > 1 ? Math.ceil(inDays) : inDays } })

const inHours = millis / (MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND)
if (inHours >= 1) return localize('times.hour', { values: { time: inHours > 1 ? Math.ceil(inHours) : inHours } })
if (inHours >= 1) return minimal ? `${Math.ceil(inHours)}h` : localize('times.hour', { values: { time: inHours > 1 ? Math.ceil(inHours) : inHours } })

const inMinutes = millis / (SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND)
if (inMinutes >= 1)
return localize('times.minute', { values: { time: inMinutes > 1 ? Math.ceil(inMinutes) : inMinutes } })
if (inMinutes >= 1) return minimal ? `${Math.ceil(inMinutes)}m` : localize('times.minute', { values: { time: inMinutes > 1 ? Math.ceil(inMinutes) : inMinutes } })

const inSeconds = millis / MILLISECONDS_PER_SECOND
if (inSeconds >= 1)
return localize('times.second', { values: { time: inSeconds > 1 ? Math.ceil(inSeconds) : inSeconds } })
if (inSeconds >= 1) return minimal ? `${Math.ceil(inSeconds)}s` : localize('times.second', { values: { time: inSeconds > 1 ? Math.ceil(inSeconds) : inSeconds } })

return zeroTime
}
Expand Down
7 changes: 7 additions & 0 deletions packages/shared/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,13 @@
"body": "A new Bloom update is available today.",
"button": "Update now"
}
},
"dappNotifications": {
"title": "Notifications",
"notEnabledHint": "Receiving notifications not enabled",
"empty": "No notifications",
"enable": "Enable notifications",
"viewAll": "View all notifications"
}
},
"picker": {
Expand Down
Loading