Preferiti, assegnazione e allegati in TAMSIV
Ci sono le grandi funzionalità e ci sono le "piccole" aggiunte che rendono il prodotto veramente utilizzabile. I preferiti, l'assegnazione dei compiti, gli allegati: ognuno sembra banale sulla carta. In pratica, ognuno mi ha confrontato con scelte architetturali che avranno conseguenze per anni.
Quando costruisci un'app da solo, ogni decisione tecnica è una scommessa. Non hai un team con cui discutere, nessun CTO per convalidare. Tu scegli, ti assumi la responsabilità, ci vivi. Queste tre funzionalità mi hanno costretto a prendere decisioni su questioni fondamentali: JSONB o tabelle relazionali? Filtro semplice o combinatorio? URL permanente o firmato? Ecco il dietro le quinte di queste scelte.
Punti chiave
- Un semplice preferito (un booleano) richiede colonna DB, RLS, filtro feed, animazione e sincronizzazione Realtime per essere production-ready.
- L'assegnazione dei compiti utilizza una tabella di collegamento
collaborative.task_assignmentscombinata con un FilterBar a 3 modalità per la massima flessibilità.- Gli allegati utilizzano tabelle relazionali invece di JSONB, una scelta architetturale che favorisce le prestazioni e la scalabilità.
- Gli URL firmati Supabase Storage scadono dopo 1 ora: un refresh automatico in batch tramite
storage_pathrisolve il problema in modo trasparente.
Perché un semplice preferito richiede così tanto tempo per essere implementato?
Mettere un'attività tra i preferiti è un toggle. Un booleano. is_favorite: true/false. Dovrebbe richiedere 30 minuti. In pratica, ci ho passato un giorno e mezzo. Ecco perché.
Innanzitutto, la colonna nel database. Aggiungere un booleano a privat.tasks è veloce. Ma è necessario aggiornare anche i RLS (Row Level Security) in modo che solo il proprietario possa modificare il suo preferito. Poi, il feed deve riflettere il cambiamento: le attività preferite devono poter essere filtrate. Ciò significa modificare la RPC get_consolidated_feed per accettare un parametro di filtro aggiuntivo.
Poi c'è l'animazione. Una stella che appare senza vita è noiosa. Ho implementato un rimbalzo con Animated.spring: la stella si ingrandisce leggermente oltre la sua dimensione finale e poi torna indietro. È sottile, 200 millisecondi in totale, ma offre un feedback soddisfacente. Gli studi del Nielsen Norman Group sulle micro-interazioni mostrano che queste animazioni di feedback migliorano significativamente la soddisfazione dell'utente.
Infine, la sincronizzazione Realtime. Se metti un'attività tra i preferiti sul tuo telefono, il cambiamento deve riflettersi immediatamente sulla dashboard web. Grazie al ContentCacheService e al suo canale Supabase Realtime, è automatico. Ma è stato necessario assicurarsi che l'evento Realtime includesse il campo is_favorite nel payload.
30 minuti sulla carta. Un giorno e mezzo in realtà. Questa è la differenza tra "implementare" e "implementare correttamente".
Come funziona il sistema di assegnazione dei compiti?
L'assegnazione è il cuore della collaborazione. Tu crei un'attività in un gruppo e la affidi a un membro. Il modello di dati si basa su una tabella di collegamento: collaborative.task_assignments. Un'attività può essere assegnata a più persone. Una persona può avere più attività assegnate.
La vera sfida tecnica non è la tabella. È il FilterBar. L'interfaccia offre tre modalità di filtraggio:
- Tutto: tutte le attività del gruppo, indipendentemente dall'autore o dall'assegnatario.
- Create da me: solo le attività che ho creato, comprese quelle assegnate ad altri.
- Assegnate a me: solo le attività che mi sono state assegnate, create da altri o da me stesso.
Queste tre modalità si combinano con il filtro gerarchico dei gruppi. Se hai un gruppo "Azienda" con sottogruppi "Marketing", "Sviluppo", "Design", puoi vedere le attività assegnate a te nel gruppo "Azienda" e tutti i suoi figli, o solo in "Marketing". Avevo dettagliato questa architettura gerarchica nell'articolo sui gruppi gerarchici.
La query SQL risultante è un join tra privat.tasks, collaborative.task_assignments e collaborative.groups con una CTE ricorsiva per la gerarchia. Secondo la documentazione PostgreSQL sulle CTE ricorsive, questo è l'approccio raccomandato per le strutture ad albero. Le prestazioni rimangono eccellenti grazie agli indici sulle chiavi esterne.
Perché scegliere le tabelle relazionali piuttosto che JSONB per gli allegati?
È una scelta architetturale fondamentale. Due approcci mi si presentavano per archiviare gli allegati (foto, video, documenti allegati a compiti e memo):
Opzione A: JSONB. Semplice e veloce. Un campo attachments JSONB direttamente nella tabella privat.tasks. Nessun join, nessuna tabella aggiuntiva. Serializzi un array di oggetti ed è fatto.
Opzione B: Tabelle relazionali. Due tabelle dedicate: privat.task_attachments e privat.memo_attachments. Ogni allegato è un record con le proprie colonne: storage_path, file_name, file_type, file_size, created_at.
Ho scelto l'opzione B. Ecco perché:
- Performance delle query: cercare "tutte le immagini di più di 5 MB" in un JSONB richiede un
jsonb_array_elementsseguito da un cast. Con una tabella relazionale, è un sempliceWHERE file_size > 5000000 AND file_type LIKE 'image/%'. - Cancellazione a cascata: quando elimini un'attività, il
ON DELETE CASCADEsulla chiave esterna pulisce automaticamente gli allegati. Con JSONB, devi gestire la pulizia manualmente. - RLS individuali: ogni allegato ha le proprie regole di accesso. Puoi autorizzare la lettura di un allegato a un gruppo senza dare accesso all'intera attività. Impossibile con JSONB.
- Scalabilità: aggiungere metadati (dimensioni dell'immagine, durata del video, miniatura) si fa con un semplice
ALTER TABLE ADD COLUMN. Con JSONB, modifichi uno schema implicito senza convalida lato DB.
Secondo le raccomandazioni PostgreSQL su JSONB, questo tipo è ideale per i dati semi-strutturati il cui schema è imprevedibile. Gli allegati hanno uno schema perfettamente prevedibile. La scelta era chiara.
Qual è la trappola degli URL firmati Supabase Storage?
Supabase Storage utilizza URL firmati per proteggere l'accesso ai file. Non puoi accedere direttamente al file tramite un URL pubblico: devi richiedere un URL temporaneo, firmato con un token, che scade dopo un periodo configurabile (per impostazione predefinita, 1 ora).
Questo è eccellente per la sicurezza. È un incubo per l'UX se non lo gestisci correttamente. Immagina: un utente apre il suo feed, vede le immagini delle sue attività. Lascia l'app aperta per 2 ore. Scorrendo: le immagini mostrano errori 403. L'URL è scaduto. L'immagine è ancora lì nello storage, ma il link per accedervi non è più valido.
La mia soluzione: StorageService.refreshAttachmentUrlsBatch(). Questo metodo prende un array di storage_path (i percorsi permanenti nel bucket Supabase) e rigenera gli URL firmati in batch. Il punto chiave: non memorizziamo mai l'URL firmato come fonte di verità. Memorizziamo il storage_path e generiamo l'URL firmato su richiesta.
Il refresh si attiva in tre casi:
- Al caricamento di un elenco: gli URL vengono generati in batch per tutti gli allegati visibili.
- Al pull-to-refresh: l'utente forza l'aggiornamento.
- Dopo un ritorno in primo piano: se l'app è rimasta in background per più di un'ora, gli URL vengono rigenerati.
Questo è il tipo di dettaglio invisibile quando funziona, e catastrofico quando si rompe. Avevo incontrato lo stesso tipo di sfida con la cache del feed e la gamification: il dato esiste, ma la sua visualizzazione dipende da un meccanismo di aggiornamento affidabile.
Come è stata progettata l'animazione della stella preferita?
L'animazione della stella è un buon esempio di micro-interazione che fa la differenza. Il principio è semplice: quando tocchi la stella, passa da vuota a piena con un effetto di rimbalzo.
Tecnicamente, è un Animated.spring con un toValue di 1.3 (overshoot del 30%) e poi un ritorno a 1.0. Il useNativeDriver: true garantisce che l'animazione giri sul thread nativo, non sul bridge JavaScript. È la stessa filosofia del pulsante nebulosa IA: le animazioni devono essere fluide anche su dispositivi di fascia bassa.
Ho aggiunto un leggero effetto aptico su iOS (tramite ReactNativeHapticFeedback) sincronizzato con il picco del rimbalzo. Su Android, il feedback aptico è meno affidabile a seconda dei produttori, quindi ho optato per un cambio di colore istantaneo: la stella passa dal grigio all'oro senza transizione di colore, solo la dimensione è animata.
La differenza tra un'app amatoriale e un'app professionale si gioca spesso in questi dettagli. Ogni interazione deve dare un feedback. L'utente deve sentire che l'app ha capito la sua azione prima ancora che il server confermi.
Come gestisce il FilterBar la complessità combinatoria?
Il FilterBar è probabilmente il componente più sottovalutato di TAMSIV. In superficie, è una riga di pulsanti. Sotto, è un sistema di filtraggio combinatorio che gestisce 3 contesti (Privato / Condiviso / Tutto) moltiplicato per 3 modalità (Tutto / Create da me / Assegnate a me) moltiplicato per N gruppi con gerarchia.
Il componente HierarchicalGroupPicker mostra l'albero dei gruppi con un toggle "includi sottogruppi". Quando selezioni un gruppo padre con l'inclusione attivata, la query utilizza una CTE ricorsiva per recuperare tutti gli ID figli. Avevo impostato questa architettura nell'articolo sui gruppi gerarchici.
La sfida principale è la performance. Ogni cambio di filtro attiva una nuova query. Se l'utente tocca rapidamente più filtri, non vogliamo 5 query concorrenti. Ho implementato un debounce di 300ms: solo l'ultimo stato del filtro attiva la query. Il risultato viene visualizzato tramite il ContentCacheService che verifica prima la cache L1 in memoria prima di andare a cercare nel DB.
Qual è l'impatto di queste "piccole" funzionalità sulla fidelizzazione?
I preferiti, l'assegnazione e gli allegati non sono funzionalità che fanno scaricare un'app. Nessuno cerca "app di attività con animazione a stella" sul Play Store. Ma sono funzionalità che fanno mantenere un'app.
Secondo un'analisi di AppsFlyer sulla fidelizzazione mobile, il tasso medio di fidelizzazione a 30 giorni per le app di produttività è del 4,5%. Le app che si distinguono sono quelle che riducono l'attrito nei flussi di lavoro quotidiani. Contrassegnare un'attività come preferita con un tocco, assegnare un'attività a un collega senza uscire dal contesto, vedere l'immagine allegata senza cliccare su un link esterno: è meno attrito.
Questa è la filosofia che stavo già perseguendo nell'articolo sulla ricerca contestuale e lo swipe: ogni interazione risparmiata è un punto di attrito eliminato. E nella produttività, l'attrito è il nemico numero 1 dell'adozione.
Domande frequenti
È possibile filtrare le attività preferite nell'agenda?
Sì. Il filtro preferito è disponibile nel feed e nell'agenda. Le attività contrassegnate come preferite appaiono con l'icona a stella in tutte le viste in cui sono visualizzate, incluso il calendario con i suoi filtri avanzati.
Quante persone possono essere assegnate a un'attività?
Non c'è un limite tecnico. La tabella di collegamento collaborative.task_assignments consente tutte le assegnazioni necessarie. In pratica, assegnare più di 5 persone alla stessa attività diventa difficile da seguire, ma il sistema lo supporta.
Gli allegati hanno un limite di dimensione?
Sì. Il piano Free consente file fino a 5 MB. Il piano Pro arriva a 25 MB. Il piano Team a 50 MB. Questi limiti sono gestiti lato frontend prima dell'upload e lato backend tramite le politiche di Supabase Storage.
Cosa succede se un URL firmato scade durante la visualizzazione?
Il StorageService rileva gli errori 403 e attiva automaticamente un refresh dell'URL. L'utente vede un breve placeholder di caricamento e poi l'immagine riappare. Il processo è trasparente.
I preferiti sono sincronizzati tra mobile e web?
Sì, in tempo reale. Il ContentCacheService utilizza i canali Supabase Realtime per propagare i cambiamenti dei preferiti tra tutti i dispositivi connessi allo stesso account.