mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
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:
parent
6fd26b91e5
commit
fe2a345651
17 changed files with 84 additions and 43 deletions
16
api/api.go
16
api/api.go
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
30
db/migrations/202508041738_app_last_used.go
Normal file
30
db/migrations/202508041738_app_last_used.go
Normal 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
|
||||
},
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ type App struct {
|
|||
WalletPubkey *string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
LastUsedAt *time.Time
|
||||
Isolated bool
|
||||
Metadata datatypes.JSON
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export interface App {
|
|||
walletPubkey: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastEventAt?: string;
|
||||
lastUsedAt?: string;
|
||||
expiresAt?: string;
|
||||
isolated: boolean;
|
||||
balance: number;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue