fix: prevent pre-existing event notifications on group join

joinedAt was stored on the group record, but foregroundSync and
resyncGroup overwrote it from the Autobase view (which has no
joinedAt). Now joinedAt is persisted as a dedicated Hyperbee key
(joinedAt:{groupId}) that survives all group record writes.

Also preserves joinedAt on group record in foregroundSync/resyncGroup
for member-filtering logic, updates iOS build instructions to prevent
bundle caching issues (--checksum rsync, sim bundle sync).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-04-10 15:07:43 -05:00
co-authored by Claude Opus 4.6
parent cd3832b34d
commit eaafe11a11
2 changed files with 17 additions and 4 deletions
+5 -2
View File
@@ -64,7 +64,7 @@ ssh Tims-Mac-mini.local 'security unlock-keychain -p "" ~/Library/Keychains/buil
npx esbuild src/ui/main.jsx --bundle --format=iife --jsx=automatic \
--define:process.env.NODE_ENV=\"production\" --outfile=assets/app-ui.bundle
# 2. Sync, build, package, install
rsync -az --exclude='.git' --exclude='node_modules' --exclude='android' \
rsync -az --checksum --exclude='.git' --exclude='node_modules' --exclude='android' \
/home/tim/peerloomllc/pearcal-native/ \
Tims-Mac-mini.local:~/peerloomllc/pearcal-native/
ssh Tims-Mac-mini.local 'export PATH="/opt/homebrew/bin:$PATH" && export LANG=en_US.UTF-8 && \
@@ -84,6 +84,7 @@ ideviceinstaller install /tmp/PearCal-release.ipa
```bash
node_modules/.bin/bare-pack --linked src/bare.js -o assets/bare-universal.bundle
node_modules/.bin/bare-pack --host ios-arm64 --linked src/bare.js -o assets/bare-ios.bundle
cp assets/bare-ios.bundle assets/bare-ios-sim.bundle
npx esbuild src/ui/main.jsx --bundle --format=iife --jsx=automatic \
--define:process.env.NODE_ENV=\"production\" --outfile=assets/app-ui.bundle
# Then sync + build as above
@@ -91,7 +92,7 @@ npx esbuild src/ui/main.jsx --bundle --format=iife --jsx=automatic \
**Swift/native module changes** (also runs `pod install` first):
```bash
rsync -az --exclude='.git' --exclude='node_modules' --exclude='android' \
rsync -az --checksum --exclude='.git' --exclude='node_modules' --exclude='android' \
/home/tim/peerloomllc/pearcal-native/ \
Tims-Mac-mini.local:~/peerloomllc/pearcal-native/
ssh Tims-Mac-mini.local 'export PATH="/opt/homebrew/bin:$PATH" && export LANG=en_US.UTF-8 && \
@@ -107,6 +108,8 @@ rsync -az Tims-Mac-mini.local:/tmp/PearCal-release.ipa /tmp/
ideviceinstaller install /tmp/PearCal-release.ipa
```
**iOS bundle caching:** Expo's asset cache survives install-over-top. If iOS behaves differently than expected after a deploy, do a full uninstall first: `ideviceinstaller uninstall com.pearcal` (this wipes app data). Also always keep `bare-ios-sim.bundle` in sync with `bare-ios.bundle` and use `--checksum` with rsync.
**Note:** New Swift/`.m` files must be registered in `PearCal.xcodeproj/project.pbxproj` via the `xcodeproj` Ruby gem before building. See plan tasks for the helper script.
**Release (App Store):** Archive and export from Xcode on the Mac Mini. Increment `buildNumber` in `app.json` before each upload.
+12 -2
View File
@@ -287,6 +287,7 @@ async function reinviteMember (groupId, memberId) {
async function deleteGroup (id) {
await db.del(NS.groups + id)
await db.del('joinedAt:' + id).catch(() => {})
// Clean up member records
for await (const { key } of db.createReadStream({ gt: NS.members + id, lt: NS.members + id + '\xff' })) {
await db.del(key)
@@ -342,6 +343,13 @@ const pendingMemberLeaves = new Set() // {groupId,memberId} JSON strings, pendi
async function joinGroup (group) {
if (bases.has(group.id)) return
// Persist joinedAt as a dedicated key so it survives group record overwrites
const joinedAtKey = 'joinedAt:' + group.id
const existingJoin = await db.get(joinedAtKey).catch(() => null)
if (!existingJoin) {
await db.put(joinedAtKey, { ts: group.joinedAt || Date.now() })
}
console.log('Joining group swarm:', group.id)
const profile = await getProfile()
@@ -563,6 +571,7 @@ async function foregroundSync () {
name: gNode.value.name || lv?.name,
emoji: gNode.value.emoji || lv?.emoji,
icon: gNode.value.icon ?? lv?.icon,
joinedAt: lv?.joinedAt || gNode.value.joinedAt,
nickname: lv?.nickname || gNode.value.nickname,
})
send({ type: 'event', event: 'sync', data: groupId })
@@ -635,6 +644,7 @@ async function resyncGroup (groupId) {
name: value.name || ev?.name,
emoji: value.emoji || ev?.emoji,
icon: value.icon ?? ev?.icon,
joinedAt: ev?.joinedAt || value.joinedAt,
removedMembers: [...removedMap.values()],
members: [...mergedMap.values()]
})
@@ -798,8 +808,8 @@ function makeApply (groupId) {
const eventDate = val.value.date
if (eventDate && eventDate < new Date().toISOString().slice(0, 10)) continue
// Skip notifications for events that predate our join (initial sync flood)
const localGroupJoin = await db.get(NS.groups + groupId).catch(() => null)
const joinedAt = localGroupJoin?.value?.joinedAt ?? 0
const joinNode = await db.get('joinedAt:' + groupId).catch(() => null)
const joinedAt = joinNode?.value?.ts ?? 0
if (joinedAt && val.value.updatedAt && val.value.updatedAt < joinedAt) continue
// Skip notification if only color changed
const onlyColorChanged = localPrev &&