feat: add app last used time (#1498)

* feat: add app last used time

* chore: use app last used instead of fetching last event

* chore: rename last_used to last_used_at, remove unused code

* fix: prettier
This commit is contained in:
Roland 2025-07-14 15:26:29 +07:00 committed by GitHub
parent 6fd26b91e5
commit fe2a345651
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 84 additions and 43 deletions

View file

@ -333,9 +333,6 @@ func (api *api) DeleteLightningAddress(ctx context.Context, appId uint) error {
func (api *api) GetApp(dbApp *db.App) *App {
var lastEvent db.RequestEvent
lastEventResult := api.db.Where("app_id = ?", dbApp.ID).Order("id desc").Limit(1).Find(&lastEvent)
paySpecificPermission := db.AppPermission{}
appPermissions := []db.AppPermission{}
var expiresAt *time.Time
@ -389,18 +386,14 @@ func (api *api) GetApp(dbApp *db.App) *App {
Metadata: metadata,
WalletPubkey: walletPubkey,
UniqueWalletPubkey: uniqueWalletPubkey,
LastUsedAt: dbApp.LastUsedAt,
}
if dbApp.Isolated {
response.Balance = queries.GetIsolatedBalance(api.db, dbApp.ID)
}
if lastEventResult.RowsAffected > 0 {
response.LastEventAt = &lastEvent.CreatedAt
}
return &response
}
func (api *api) ListApps() ([]App, error) {
@ -442,6 +435,7 @@ func (api *api) ListApps() ([]App, error) {
Isolated: dbApp.Isolated,
WalletPubkey: walletPubkey,
UniqueWalletPubkey: uniqueWalletPubkey,
LastUsedAt: dbApp.LastUsedAt,
}
if dbApp.Isolated {
@ -458,12 +452,6 @@ func (api *api) ListApps() ([]App, error) {
}
}
var lastEvent db.RequestEvent
lastEventResult := api.db.Where("app_id = ?", dbApp.ID).Order("id desc").Limit(1).Find(&lastEvent)
if lastEventResult.RowsAffected > 0 {
apiApp.LastEventAt = &lastEvent.CreatedAt
}
var metadata Metadata
if dbApp.Metadata != nil {
jsonErr := json.Unmarshal(dbApp.Metadata, &metadata)

View file

@ -88,7 +88,7 @@ type App struct {
AppPubkey string `json:"appPubkey"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
LastEventAt *time.Time `json:"lastEventAt"`
LastUsedAt *time.Time `json:"lastUsedAt"`
ExpiresAt *time.Time `json:"expiresAt"`
Scopes []string `json:"scopes"`
MaxAmountSat uint64 `json:"maxAmount"`

View file

@ -7,6 +7,7 @@ import (
"gorm.io/gorm"
)
// NOTE: This was actually 2025-07-08
var _202508041712_delete_non_cascade_deleted_records = &gormigrate.Migration{
ID: "202508041712_delete_non_cascade_deleted_records",
Migrate: func(db *gorm.DB) error {

View file

@ -7,6 +7,7 @@ import (
"gorm.io/gorm"
)
// NOTE: This was actually 2025-07-08
var _202508041737_postgres_amount_bigint = &gormigrate.Migration{
ID: "202508041737_postgres_amount_bigint",
Migrate: func(db *gorm.DB) error {

View file

@ -0,0 +1,30 @@
package migrations
import (
_ "embed"
"text/template"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
const appLastUsedMigration = `ALTER TABLE apps ADD COLUMN last_used_at {{ .Timestamp }};`
var appLastUsedMigrationTmpl = template.Must(template.New("appLastUsedMigration").Parse(appLastUsedMigration))
// NOTE: This was actually 2025-07-14
var _202508041738_app_last_used = &gormigrate.Migration{
ID: "202508041738_app_last_used",
Migrate: func(tx *gorm.DB) error {
err := exec(tx, appLastUsedMigrationTmpl)
if err != nil {
return err
}
return nil
},
Rollback: func(tx *gorm.DB) error {
return nil
},
}

View file

@ -33,6 +33,7 @@ func Migrate(gormDB *gorm.DB) error {
_202506170342_swaps,
_202508041712_delete_non_cascade_deleted_records,
_202508041737_postgres_amount_bigint,
_202508041738_app_last_used,
})
return m.Migrate()

View file

@ -23,6 +23,7 @@ type App struct {
WalletPubkey *string
CreatedAt time.Time
UpdatedAt time.Time
LastUsedAt *time.Time
Isolated bool
Metadata datatypes.JSON
}

View file

@ -238,7 +238,9 @@ function TransactionItem({ tx }: Props) {
<p>App</p>
<Link to={`/apps/${app.appPubkey}`}>
<p className="font-semibold">
{app.name === ALBY_ACCOUNT_APP_NAME ? "Alby Account" : app.name}
{app.name === ALBY_ACCOUNT_APP_NAME
? "Alby Account"
: app.name}
</p>
</Link>
</div>

View file

@ -52,8 +52,8 @@ export function AppCardConnectionInfo({
<div className="flex flex-row justify-between text-xs items-end mt-2">
<div className="text-muted-foreground">
Last used:{" "}
{connection.lastEventAt
? dayjs(connection.lastEventAt).fromNow()
{connection.lastUsedAt
? dayjs(connection.lastUsedAt).fromNow()
: "Never"}
</div>
<div className="flex flex-col items-end justify-end">
@ -89,8 +89,8 @@ export function AppCardConnectionInfo({
<div className="flex flex-row justify-between text-xs items-center text-muted-foreground mt-2">
<div>
Last used:{" "}
{connection.lastEventAt
? dayjs(connection.lastEventAt).fromNow()
{connection.lastUsedAt
? dayjs(connection.lastUsedAt).fromNow()
: "Never"}
</div>
<div>
@ -120,8 +120,8 @@ export function AppCardConnectionInfo({
<div className="flex flex-row justify-between items-center">
<div className="text-muted-foreground text-xs">
Last used:{" "}
{connection.lastEventAt
? dayjs(connection.lastEventAt).fromNow()
{connection.lastUsedAt
? dayjs(connection.lastUsedAt).fromNow()
: "Never"}
</div>
{!readonly && (
@ -157,8 +157,8 @@ export function AppCardConnectionInfo({
<div className="flex flex-row justify-between items-center">
<div className="flex flex-row justify-between text-xs items-center text-muted-foreground">
Last used:{" "}
{connection.lastEventAt
? dayjs(connection.lastEventAt).fromNow()
{connection.lastUsedAt
? dayjs(connection.lastUsedAt).fromNow()
: "Never"}
</div>
{!readonly && (

View file

@ -13,7 +13,7 @@ import { useApps } from "src/hooks/useApps";
export function LatestUsedAppsWidget() {
const { data: apps } = useApps();
const usedApps = apps?.filter((x) => x.lastEventAt);
const usedApps = apps?.filter((x) => x.lastUsedAt);
if (!usedApps?.length) {
return null;
@ -28,8 +28,8 @@ export function LatestUsedAppsWidget() {
{usedApps
.sort(
(a, b) =>
new Date(b.lastEventAt ?? 0).getTime() -
new Date(a.lastEventAt ?? 0).getTime()
new Date(b.lastUsedAt ?? 0).getTime() -
new Date(a.lastUsedAt ?? 0).getTime()
)
.slice(0, 3)
.map((app) => (
@ -37,10 +37,12 @@ export function LatestUsedAppsWidget() {
<div className="flex items-center w-full gap-4">
<AppAvatar app={app} className="w-14 h-14 rounded-lg" />
<p className="text-sm font-medium flex-1 truncate">
{app.name === ALBY_ACCOUNT_APP_NAME ? "Alby Account" : app.name}
{app.name === ALBY_ACCOUNT_APP_NAME
? "Alby Account"
: app.name}
</p>
<p className="text-xs text-muted-foreground">
{app.lastEventAt ? dayjs(app.lastEventAt).fromNow() : "never"}
{app.lastUsedAt ? dayjs(app.lastUsedAt).fromNow() : "never"}
</p>
<ChevronRightIcon className="text-muted-foreground w-8 h-8" />
</div>

View file

@ -12,7 +12,7 @@ export function useUnusedApps() {
() =>
apps?.filter(
(app) =>
(!app.lastEventAt || dayjs(app.lastEventAt).isBefore(OLD_DATE)) &&
(!app.lastUsedAt || dayjs(app.lastUsedAt).isBefore(OLD_DATE)) &&
app.metadata?.app_store_app_id != SUBWALLET_APPSTORE_APP_ID
),
[apps]

View file

@ -47,14 +47,14 @@ function AppCreatedInternal() {
const { data: app } = useApp(createAppResponse.pairingPublicKey, true);
useEffect(() => {
if (app?.lastEventAt) {
if (app?.lastUsedAt) {
toast({
title: "Connection established!",
description: "You can now use the app with your Alby Hub.",
});
navigate("/apps");
}
}, [app?.lastEventAt, navigate, toast]);
}, [app?.lastUsedAt, navigate, toast]);
useEffect(() => {
// dispatch a success event which can be listened to by the opener or by the app that embedded the webview
@ -171,7 +171,7 @@ export function ConnectAppCard({
<CardTitle className="text-center">Connection Secret</CardTitle>
</CardHeader>
<CardContent className="flex flex-col items-center gap-5">
{!app.lastEventAt ? (
{!app.lastUsedAt ? (
<>
<div className="flex flex-row items-center gap-2 text-sm">
<Loading className="w-4 h-4" />

View file

@ -6,7 +6,10 @@ import Loading from "src/components/Loading";
import ResponsiveButton from "src/components/ResponsiveButton";
import AlbyConnectionCard from "src/components/connections/AlbyConnectionCard";
import AppCard from "src/components/connections/AppCard";
import { ALBY_ACCOUNT_APP_NAME, SUBWALLET_APPSTORE_APP_ID } from "src/constants";
import {
ALBY_ACCOUNT_APP_NAME,
SUBWALLET_APPSTORE_APP_ID,
} from "src/constants";
import { useApps } from "src/hooks/useApps";
import { useInfo } from "src/hooks/useInfo";
import { useUnusedApps } from "src/hooks/useUnusedApps";
@ -28,8 +31,8 @@ function AppList() {
)
.sort(
(a, b) =>
new Date(b.lastEventAt ?? 0).getTime() -
new Date(a.lastEventAt ?? 0).getTime()
new Date(b.lastUsedAt ?? 0).getTime() -
new Date(a.lastUsedAt ?? 0).getTime()
);
return (

View file

@ -35,8 +35,8 @@ export function AppsCleanup() {
const _appsToReview = [...unusedApps];
_appsToReview.sort((a, b) => {
return (
(a.lastEventAt ? new Date(a.lastEventAt).getTime() : 0) -
(b.lastEventAt ? new Date(b.lastEventAt).getTime() : 0)
(a.lastUsedAt ? new Date(a.lastUsedAt).getTime() : 0) -
(b.lastUsedAt ? new Date(b.lastUsedAt).getTime() : 0)
);
});
setAppsToReview(_appsToReview);

View file

@ -50,7 +50,10 @@ import {
} from "src/components/ui/tooltip";
import { useToast } from "src/components/ui/use-toast";
import { UpgradeDialog } from "src/components/UpgradeDialog";
import { ALBY_ACCOUNT_APP_NAME, SUBWALLET_APPSTORE_APP_ID } from "src/constants";
import {
ALBY_ACCOUNT_APP_NAME,
SUBWALLET_APPSTORE_APP_ID,
} from "src/constants";
import { useAlbyMe } from "src/hooks/useAlbyMe";
import { useCapabilities } from "src/hooks/useCapabilities";
import { useCreateLightningAddress } from "src/hooks/useCreateLightningAddress";
@ -153,7 +156,8 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
}
};
const appName = app.name === ALBY_ACCOUNT_APP_NAME ? "Alby Account" : app.name;
const appName =
app.name === ALBY_ACCOUNT_APP_NAME ? "Alby Account" : app.name;
return (
<>
@ -367,8 +371,8 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
<TableRow>
<TableCell className="font-medium">Last used</TableCell>
<TableCell className="text-muted-foreground">
{app.lastEventAt
? new Date(app.lastEventAt).toString()
{app.lastUsedAt
? new Date(app.lastUsedAt).toString()
: "Never"}
</TableCell>
</TableRow>

View file

@ -119,7 +119,7 @@ export interface App {
walletPubkey: string;
createdAt: string;
updatedAt: string;
lastEventAt?: string;
lastUsedAt?: string;
expiresAt?: string;
isolated: boolean;
balance: number;

View file

@ -74,6 +74,14 @@ func (svc *nip47Service) HandleEvent(ctx context.Context, relay nostrmodels.Rela
return
}
now := time.Now()
err = svc.db.Model(&app).Update("last_used_at", &now).Error
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"it": app.ID,
}).WithError(err).Error("Failed to update app last used time")
}
logger.Logger.WithFields(logrus.Fields{
"requestEventNostrId": event.ID,
"eventKind": event.Kind,