Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
74e8a9a
Speed up devices list by using local cache for lastUsedTime
chrisnojima Jun 2, 2026
cbbd46b
Address PR feedback: fix misleading comment and retry on sync failure
chrisnojima Jun 2, 2026
052cb63
Address PR feedback: consistent unknown lastUsed UX, targeted notify
chrisnojima Jun 2, 2026
a69013a
Address PR feedback: uid-filter keyfamilyChanged, fix UID source, cla…
chrisnojima Jun 3, 2026
3b13f2b
Address PR feedback: use defer for mutex unlock
chrisnojima Jun 3, 2026
1e91361
Address PR feedback: remove unnecessary closure around mutex
chrisnojima Jun 3, 2026
8a06fd6
Add NotifyDeviceHistory notification; replace keyfamilyChanged with d…
chrisnojima Jun 3, 2026
86ebc7d
Wire up deviceHistoryChanged notification on TS side
chrisnojima Jun 3, 2026
cbd57c6
Address PR feedback: merge redundant shouldSync conditionals
chrisnojima Jun 3, 2026
b2edc8c
Fix stale comment referencing keyfamilyChanged
chrisnojima Jun 3, 2026
e04d971
Revert keyfamily: true; subscribe to devicehistory instead
chrisnojima Jun 3, 2026
fdbfc58
Remove keyfamilyChanged RPC wiring added by this PR; not needed
chrisnojima Jun 3, 2026
c72c494
Regenerate protocol files via make build; add deviceHistoryChanged to…
chrisnojima Jun 3, 2026
e048cf2
rerun gen
chrisnojima Jun 3, 2026
6d00131
Potential fix for pull request finding
chrisnojima Jun 3, 2026
cd914ae
Merge branch 'nojima/HOTPOT-next-670-clean-2' into nojima/HOTPOT-fast…
chrisnojima Jun 3, 2026
4de6dba
format
chrisnojima Jun 3, 2026
fb5613e
regen
chrisnojima Jun 3, 2026
6af1c94
Fix TestDeviceHistoryBasic: force sync when active device missing fro…
chrisnojima Jun 3, 2026
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
7 changes: 7 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
{
"pluginConfigs": {
"typescript-lsp@claude-plugins-official": {
"options": {
"tsserver.path": "shared/node_modules/typescript/bin/tsserver"
}
}
},
"permissions": {
"allow": [
"Bash(yarn lint)",
Expand Down
48 changes: 42 additions & 6 deletions go/engine/device_history.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,26 @@ func (e *DeviceHistory) loadDevices(m libkb.MetaContext, user *libkb.User) error
if err != nil {
return err
}
// If any active device is absent from the cache (e.g. a paper key
// provisioned after the last secrets sync), force a network sync so
// every device gets a real LastUsedTime on this first load.
for _, detail := range e.devices {
if detail.RevokedAt != nil {
continue
}
if _, ok := lastUsedTimes[detail.Device.DeviceID]; !ok {
lastUsedTimes, err = e.getLastUsedTimesForce(m)
if err != nil {
return err
}
break
}
}
for i := range e.devices {
detail := &e.devices[i]
lastUsedTime, ok := lastUsedTimes[detail.Device.DeviceID]
if !ok {
if detail.RevokedAt != nil {
// The server only provides last used times for active devices.
continue
}
return fmt.Errorf("Failed to load last used time for device %s", detail.Device.DeviceID)
continue
}
detail.Device.LastUsedTime = keybase1.TimeFromSeconds(lastUsedTime.Unix())
}
Expand Down Expand Up @@ -183,10 +194,18 @@ func (e *DeviceHistory) getLastUsedTimes(m libkb.MetaContext) (ret map[keybase1.
defer m.Trace("DeviceHistory#getLastUsedTimes", &err)()
var devs libkb.DeviceKeyMap
var ss *libkb.SecretSyncer
ss, err = m.ActiveDevice().SyncSecretsForce(m)
// Use local cache to avoid a blocking network call on every page load.
// Falls back to a forced network sync when the cache is empty.
ss, err = m.ActiveDevice().SyncSecretsFromCache(m)
Comment thread
chrisnojima marked this conversation as resolved.
Comment thread
chrisnojima marked this conversation as resolved.
if err != nil {
return nil, err
}
if !ss.HasDevices() {
ss, err = m.ActiveDevice().SyncSecretsForce(m)
if err != nil {
return nil, err
}
}
devs, err = ss.ActiveDevices(libkb.AllDeviceTypes)
if err != nil {
return nil, err
Expand All @@ -197,3 +216,20 @@ func (e *DeviceHistory) getLastUsedTimes(m libkb.MetaContext) (ret map[keybase1.
}
return ret, nil
}

func (e *DeviceHistory) getLastUsedTimesForce(m libkb.MetaContext) (ret map[keybase1.DeviceID]time.Time, err error) {
defer m.Trace("DeviceHistory#getLastUsedTimesForce", &err)()
ss, err := m.ActiveDevice().SyncSecretsForce(m)
if err != nil {
return nil, err
}
devs, err := ss.ActiveDevices(libkb.AllDeviceTypes)
if err != nil {
return nil, err
}
ret = map[keybase1.DeviceID]time.Time{}
for deviceID, dev := range devs {
ret[deviceID] = time.Unix(dev.LastUsedTime, 0)
}
return ret, nil
}
18 changes: 18 additions & 0 deletions go/libkb/active_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,24 @@ func (a *ActiveDevice) SyncSecretsForce(m MetaContext) (ret *SecretSyncer, err e
return a.SyncSecretsForUID(m, zed, true /* force */)
}

func (a *ActiveDevice) SyncSecretsFromCache(m MetaContext) (ret *SecretSyncer, err error) {
defer m.Trace("ActiveDevice#SyncSecretsFromCache", &err)()
a.RLock()
s := a.secretSyncer
uid := a.uv.Uid
a.RUnlock()
if s == nil {
return nil, fmt.Errorf("Can't sync secrets: nil secret syncer")
}
if uid.IsNil() {
return nil, fmt.Errorf("can't run secret syncer without a UID")
}
if err = RunSyncerCached(m, s, uid); err != nil {
return nil, err
}
return s, nil
}

func (a *ActiveDevice) CheckForUsername(m MetaContext, n NormalizedUsername, suppressNetworkErrors bool) (err error) {
a.RLock()
uid := a.uv.Uid
Expand Down
26 changes: 26 additions & 0 deletions go/libkb/notify_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type NotifyListener interface {
SimpleFSArchiveStatusChanged(status keybase1.SimpleFSArchiveStatus)
PaperKeyCached(uid keybase1.UID, encKID keybase1.KID, sigKID keybase1.KID)
KeyfamilyChanged(uid keybase1.UID)
DeviceHistoryChanged()
NewChatActivity(uid keybase1.UID, activity chat1.ChatActivity, source chat1.ChatActivitySource)
NewChatKBFSFileEditActivity(uid keybase1.UID, activity chat1.ChatActivity)
ChatIdentifyUpdate(update keybase1.CanonicalTLFNameAndIDWithBreaks)
Expand Down Expand Up @@ -154,6 +155,7 @@ func (n *NoopNotifyListener) SimpleFSArchiveStatusChanged(status keybase1.Simple
func (n *NoopNotifyListener) PaperKeyCached(uid keybase1.UID, encKID keybase1.KID, sigKID keybase1.KID) {
}
func (n *NoopNotifyListener) KeyfamilyChanged(uid keybase1.UID) {}
func (n *NoopNotifyListener) DeviceHistoryChanged() {}
func (n *NoopNotifyListener) NewChatActivity(uid keybase1.UID, activity chat1.ChatActivity,
source chat1.ChatActivitySource) {
}
Expand Down Expand Up @@ -2049,6 +2051,30 @@ func (n *NotifyRouter) HandleKeyfamilyChanged(uid keybase1.UID) {
n.G().Log.Debug("- Sent keyfamily changed notification")
}

// HandleDeviceHistoryChanged is called whenever the device history list is refreshed.
func (n *NotifyRouter) HandleDeviceHistoryChanged() {
if n == nil {
return
}

n.G().Log.Debug("+ Sending device history changed notification")
n.cm.ApplyAll(func(id ConnectionID, xp rpc.Transporter) bool {
if n.getNotificationChannels(id).Devicehistory {
go func() {
_ = (keybase1.NotifyDeviceHistoryClient{
Cli: rpc.NewClient(xp, NewContextifiedErrorUnwrapper(n.G()), nil),
}).DeviceHistoryChanged(context.Background())
}()
}
return true
})

n.runListeners(func(listener NotifyListener) {
listener.DeviceHistoryChanged()
})
n.G().Log.Debug("- Sent device history changed notification")
}

// HandleServiceShutdown is called whenever the service shuts down.
func (n *NotifyRouter) HandleServiceShutdown() {
if n == nil {
Expand Down
2 changes: 2 additions & 0 deletions go/protocol/keybase1/notify_ctl.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions go/protocol/keybase1/notify_device_history.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 31 additions & 1 deletion go/service/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"context"
"encoding/json"
"errors"
"sync"
"time"

"github.com/keybase/client/go/engine"
"github.com/keybase/client/go/gregor"
Expand All @@ -15,11 +17,15 @@ import (
"github.com/keybase/go-framed-msgpack-rpc/rpc"
)

const deviceSyncTTL = 30 * time.Second

// DeviceHandler is the RPC handler for the device interface.
type DeviceHandler struct {
*BaseHandler
libkb.Contextified
gregor *gregorHandler
gregor *gregorHandler
syncMu sync.Mutex
lastDeviceSync time.Time
}

// NewDeviceHandler creates a DeviceHandler for the xp transport.
Expand Down Expand Up @@ -57,9 +63,33 @@ func (h *DeviceHandler) DeviceHistoryList(nctx context.Context, sessionID int) (
if err := engine.RunEngine2(m, eng); err != nil {
return nil, err
}
// After returning cached data quickly, refresh secrets in the background
// so lastUsedTime values stay current. Fires deviceHistoryChanged when done
// so the UI reloads. Debounced to avoid redundant syncs.
h.syncMu.Lock()
defer h.syncMu.Unlock()
if time.Since(h.lastDeviceSync) > deviceSyncTTL {
h.lastDeviceSync = time.Now()
go h.backgroundSyncDevices()
}
Comment thread
chrisnojima marked this conversation as resolved.
return eng.Devices(), nil
}

func (h *DeviceHandler) backgroundSyncDevices() {
mctx := libkb.NewMetaContext(context.Background(), h.G())
if _, err := mctx.ActiveDevice().SyncSecretsForce(mctx); err != nil {
mctx.Debug("DeviceHandler#backgroundSyncDevices error: %v", err)
// Reset so the next UI refresh can retry instead of waiting out the TTL.
h.syncMu.Lock()
defer h.syncMu.Unlock()
h.lastDeviceSync = time.Time{}
return
Comment thread
chrisnojima marked this conversation as resolved.
}
if nr := h.G().NotifyRouter; nr != nil {
nr.HandleDeviceHistoryChanged()
}
Comment thread
chrisnojima marked this conversation as resolved.
}

// DeviceAdd starts the kex2 device provisioning on the
// provisioner (device X/C1)
func (h *DeviceHandler) DeviceAdd(c context.Context, sessionID int) error {
Expand Down
1 change: 1 addition & 0 deletions protocol/avdl/keybase1/notify_ctl.avdl
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ protocol notifyCtl {
// and can skip updates for things not currently on the screen.
boolean allowChatNotifySkips;
boolean chatarchive;
boolean devicehistory;
}

void setNotifications(NotificationChannels channels);
Expand Down
4 changes: 4 additions & 0 deletions protocol/avdl/keybase1/notify_device_history.avdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@namespace("keybase.1")
protocol NotifyDeviceHistory {
void deviceHistoryChanged() oneway;
}
1 change: 1 addition & 0 deletions protocol/bin/enabled-calls.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
"keybase.1.NotifyAudit.boxAuditError": {"incoming":true},
"keybase.1.NotifyAudit.rootAuditError": {"incoming":true},
"keybase.1.NotifyBadges.badgeState": {"incoming":true},
"keybase.1.NotifyDeviceHistory.deviceHistoryChanged": {"incoming":true},
"keybase.1.NotifyEmailAddress.emailAddressVerified": {"custom":true},
"keybase.1.NotifyEmailAddress.emailsChanged": {"custom":true},
"keybase.1.NotifyFS.FSActivity": {"incoming":true},
Expand Down
4 changes: 4 additions & 0 deletions protocol/json/keybase1/notify_ctl.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions protocol/json/keybase1/notify_device_history.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 3 additions & 27 deletions shared/common-adapters/markdown/emoji-gen.tsx

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion shared/constants/init/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ export const onEngineConnected = () => {
allowChatNotifySkips: true, app: true, audit: true, badges: true, chat: true, chatarchive: true,
chatattachments: true, chatdev: false, chatemoji: false, chatemojicross: false, chatkbfsedits: false,
deviceclone: false, ephemeral: false, favorites: false, featuredBots: false, kbfs: true, kbfsdesktop: !isMobile,
kbfslegacy: false, kbfsrequest: false, kbfssubscription: true, keyfamily: false, notifysimplefs: true,
devicehistory: true, kbfslegacy: false, kbfsrequest: false, kbfssubscription: true, keyfamily: false, notifysimplefs: true,
paperkeys: false, pgp: true, reachability: true, runtimestats: true, saltpack: true, service: true, session: true,
team: true, teambot: false, tracking: true, users: true, wallet: false,
},
Expand Down
1 change: 1 addition & 0 deletions shared/constants/rpc/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type Keybase1IncomingAction =
'keybase.1.NotifyAudit.boxAuditError' |
'keybase.1.NotifyAudit.rootAuditError' |
'keybase.1.NotifyBadges.badgeState' |
'keybase.1.NotifyDeviceHistory.deviceHistoryChanged' |
'keybase.1.NotifyFS.FSActivity' |
'keybase.1.NotifySession.loggedOut' |
'keybase.1.NotifyTracking.trackingChanged' |
Expand Down
Loading