Skip to content
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
29 changes: 21 additions & 8 deletions frontend-v2/src/app/(authed)/events/activist-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,25 @@ interface SyncMetadata {
lastSyncTime: string // ISO 8601 timestamp
}

const DB_NAME = 'activist-registry'
const DB_VERSION = 2
const STORE_NAME = 'activists'
const METADATA_STORE = 'metadata'

export class ActivistStorage {
private dbPromise: Promise<IDBDatabase> | null = null
private readonly dbName: string

constructor(chapterId: number) {
this.dbName = `adb-chapter-${chapterId}`
}

/**
* Initialize the IndexedDB database with required object stores.
*/
private openDB(): Promise<IDBDatabase> {
if (!this.dbPromise) {
this.dbPromise = new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION)
const request = indexedDB.open(this.dbName, DB_VERSION)

request.onerror = () => reject(request.error)
request.onsuccess = () => {
Expand Down Expand Up @@ -98,7 +102,7 @@ export class ActivistStorage {
return new Promise((resolve, reject) => {
const transaction = db.transaction([METADATA_STORE], 'readonly')
const store = transaction.objectStore(METADATA_STORE)
const request = store.get('lastSync')
const request = store.get('lastActivistSync')

request.onsuccess = () => {
const metadata = request.result as SyncMetadata | undefined
Expand All @@ -120,10 +124,10 @@ export class ActivistStorage {

const request =
timestamp === null
? store.delete('lastSync')
? store.delete('lastActivistSync')
: store.put(
{ lastSyncTime: timestamp } satisfies SyncMetadata,
'lastSync',
'lastActivistSync',
)

request.onsuccess = () => resolve()
Expand Down Expand Up @@ -219,6 +223,15 @@ function isIndexedDBAvailable(): boolean {
}
}

// Singleton instance - only create if IndexedDB is available
export const activistStorage: ActivistStorage | undefined =
isIndexedDBAvailable() ? new ActivistStorage() : undefined
const storageByChapter = new Map<number, ActivistStorage>()

export function getActivistStorage(
chapterId: number,
): ActivistStorage | undefined {
if (!isIndexedDBAvailable()) return undefined
const existing = storageByChapter.get(chapterId)
if (existing) return existing
const storage = new ActivistStorage(chapterId)
storageByChapter.set(chapterId, storage)
return storage
}
2 changes: 1 addition & 1 deletion frontend-v2/src/app/(authed)/events/event-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const EventForm = ({ mode }: EventFormProps) => {
// The registry loads cached data from IndexedDB first, then syncs any
// new/updated activists from the server in the background.
const { registry: activistRegistry, isLoading: isLoadingActivists } =
useActivistRegistry()
useActivistRegistry(user.ChapterID)

// Fetch existing event/connection, if editing.
const {
Expand Down
4 changes: 3 additions & 1 deletion frontend-v2/src/app/(authed)/events/events-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
} from '@/components/ui/select'
import { DatePicker } from '@/components/ui/date-picker'
import { useActivistRegistry } from './useActivistRegistry'
import { useAuthedPageContext } from '@/hooks/useAuthedPageContext'
import { SuggestionInput } from './suggestion-input'
import { EventListTable } from './event-list-table'
import { cn } from '@/lib/utils'
Expand Down Expand Up @@ -119,7 +120,8 @@ export default function EventsPage({ mode = 'events' }: Props) {
const defaultParams = useMemo(() => buildDefaultParams(mode), [mode])
const isConnections = mode === 'connections'
const queryClient = useQueryClient()
const { registry } = useActivistRegistry()
const { user } = useAuthedPageContext()
const { registry } = useActivistRegistry(user.ChapterID)

// URL params drive the query and are the source of truth for committed filters
const [urlParams, setUrlParams] = useQueryStates({
Expand Down
12 changes: 7 additions & 5 deletions frontend-v2/src/app/(authed)/events/useActivistRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react'
import { useQuery } from '@tanstack/react-query'
import { apiClient, API_PATH } from '@/lib/api'
import { ActivistRegistry, type ActivistRecord } from './activist-registry'
import { activistStorage } from './activist-storage'
import { getActivistStorage } from './activist-storage'
import toast from 'react-hot-toast'

/**
Expand All @@ -13,7 +13,7 @@ import toast from 'react-hot-toast'
*
* @returns Object containing the registry instance and query state
*/
export function useActivistRegistry() {
export function useActivistRegistry(chapterId: number) {
const registryRef = useRef(new ActivistRegistry())
const [isStorageLoaded, setIsStorageLoaded] = useState(false)
const [isServerLoaded, setIsServerLoaded] = useState(false)
Expand All @@ -22,8 +22,10 @@ export function useActivistRegistry() {
useEffect(() => {
let mounted = true

const storage = getActivistStorage(chapterId)

// If IndexedDB is not available (e.g., iOS lockdown mode), skip loading from storage
if (!activistStorage) {
if (!storage) {
console.info(
'[Registry] IndexedDB not available - running without local caching',
)
Expand All @@ -32,7 +34,7 @@ export function useActivistRegistry() {
}

registryRef.current
.loadFromStorage(activistStorage)
.loadFromStorage(storage)
.then(() => {
if (mounted) setIsStorageLoaded(true)
})
Expand All @@ -56,7 +58,7 @@ export function useActivistRegistry() {
return () => {
mounted = false
}
}, [])
}, [chapterId])

const query = useQuery({
queryKey: [API_PATH.ACTIVIST_LIST_BASIC],
Expand Down
4 changes: 1 addition & 3 deletions frontend-v2/src/components/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { usePathname, useSearchParams } from 'next/navigation'
import { useAuthedPageContext } from '@/hooks/useAuthedPageContext'
import buefyStyles from './nav.module.css'
import clsx from 'clsx'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useQuery } from '@tanstack/react-query'
import { useQueryStates } from 'nuqs'
import { API_PATH, apiClient } from '@/lib/api'
import { SF_BAY_CHAPTER_ID } from '@/lib/constants'
Expand Down Expand Up @@ -201,7 +201,6 @@ const DropdownItem = ({

const ChapterSwitcher = () => {
const { user } = useAuthedPageContext()
const queryClient = useQueryClient()

const { data, isLoading, isError } = useQuery({
queryKey: [API_PATH.CHAPTER_LIST],
Expand Down Expand Up @@ -229,7 +228,6 @@ const ChapterSwitcher = () => {
}

const switchChapter = (e: React.ChangeEvent<HTMLSelectElement>) => {
queryClient.invalidateQueries() // invalidate existing cache for previous chapter
window.location.href = `/auth/switch_chapter?chapter_id=${e.target.value}`
}

Expand Down
Loading