Favoritos, atribuição e anexos no TAMSIV
Existem os grandes recursos e existem os "pequenos" acréscimos que tornam o produto realmente utilizável. Favoritos, atribuição de tarefas, anexos: cada um parece trivial no papel. Na prática, cada um me confrontou com escolhas arquitetônicas que terão consequências por anos.
Quando você constrói um aplicativo sozinho, cada decisão técnica é uma aposta. Você não tem uma equipe para debater, nem um CTO para validar. Você escolhe, você assume, você vive com isso. Esses três recursos me forçaram a tomar decisões sobre questões fundamentais: JSONB ou tabelas relacionais? Filtro simples ou combinatório? URL permanente ou assinada? Aqui estão os bastidores dessas escolhas.
Pontos chave
- Um simples favorito (um booleano) requer coluna DB, RLS, feed de filtro, animação e sincronização em tempo real para estar pronto para produção.
- A atribuição de tarefas usa uma tabela de junção
collaborative.task_assignmentscombinada com um FilterBar de 3 modos para máxima flexibilidade.- Os anexos usam tabelas relacionais em vez de JSONB, uma escolha arquitetônica que favorece o desempenho e a escalabilidade.
- As URLs assinadas do Supabase Storage expiram após 1 hora: uma atualização automática em lote via
storage_pathresolve o problema de forma transparente.
Por que um simples favorito leva tanto tempo para ser implementado?
Marcar uma tarefa como favorita é um toggle. Um booleano. is_favorite: true/false. Deveria levar 30 minutos. Na prática, passei um dia e meio nisso. Veja o porquê.
Primeiro, a coluna no banco de dados. Adicionar um booleano a privat.tasks é rápido. Mas também é preciso atualizar o RLS (Row Level Security) para que apenas o proprietário possa modificar seu favorito. Em seguida, o feed deve refletir a mudança: as tarefas favoritas devem poder ser filtradas. Isso significa modificar o RPC get_consolidated_feed para aceitar um parâmetro de filtro adicional.
Depois, há a animação. Uma estrela que aparece sem vida é chato. Implementei um bounce com Animated.spring: a estrela cresce ligeiramente além de seu tamanho final e depois retorna. É sutil, 200 milissegundos no total, mas oferece um feedback satisfatório. Estudos do Nielsen Norman Group sobre microinterações mostram que essas animações de feedback melhoram significativamente a satisfação do usuário.
Finalmente, a sincronização em tempo real. Se você marca uma tarefa como favorita no seu telefone, a mudança deve ser refletida imediatamente no painel web. Graças ao ContentCacheService e seu canal Supabase Realtime, isso é automático. Mas foi preciso garantir que o evento Realtime incluísse o campo is_favorite no payload.
30 minutos no papel. Um dia e meio na realidade. Essa é a diferença entre "implementar" e "implementar corretamente".
Como funciona o sistema de atribuição de tarefas?
A atribuição é o coração da colaboração. Você cria uma tarefa em um grupo e a confia a um membro. O modelo de dados é baseado em uma tabela de junção: collaborative.task_assignments. Uma tarefa pode ser atribuída a várias pessoas. Uma pessoa pode ter várias tarefas atribuídas.
O verdadeiro desafio técnico não é a tabela. É o FilterBar. A interface oferece três modos de filtragem:
- Tudo: todas as tarefas do grupo, independentemente do autor ou do atribuído.
- Criadas por mim: apenas as tarefas que criei, incluindo as atribuídas a outros.
- Atribuídas a mim: apenas as tarefas que me foram atribuídas, criadas por outros ou por mim mesmo.
Esses três modos se combinam com o filtro hierárquico de grupos. Se você tem um grupo "Empresa" com subgrupos "Marketing", "Dev", "Design", você pode ver as tarefas atribuídas a você no grupo "Empresa" e todos os seus filhos, ou apenas em "Marketing". Eu detalhei essa arquitetura hierárquica no artigo sobre grupos hierárquicos.
A consulta SQL resultante é uma junção entre privat.tasks, collaborative.task_assignments e collaborative.groups com uma CTE recursiva para a hierarquia. De acordo com a documentação do PostgreSQL sobre CTEs recursivas, essa é a abordagem recomendada para estruturas em árvore. O desempenho permanece excelente graças aos índices nas chaves estrangeiras.
Por que escolher tabelas relacionais em vez de JSONB para anexos?
Esta é uma escolha arquitetônica fundamental. Duas abordagens estavam disponíveis para armazenar anexos (fotos, vídeos, documentos anexados a tarefas e memorandos):
Opção A: JSONB. Simples e rápido. Um campo attachments JSONB diretamente na tabela privat.tasks. Sem junção, sem tabela adicional. Você serializa um array de objetos e pronto.
Opção B: Tabelas relacionais. Duas tabelas dedicadas: privat.task_attachments e privat.memo_attachments. Cada anexo é um registro com suas próprias colunas: storage_path, file_name, file_type, file_size, created_at.
Escolhi a opção B. Veja o porquê:
- Desempenho de consulta: procurar "todas as imagens com mais de 5 MB" em um JSONB requer um
jsonb_array_elementsseguido de um cast. Com uma tabela relacional, é um simplesWHERE file_size > 5000000 AND file_type LIKE 'image/%'. - Exclusão em cascata: quando você exclui uma tarefa, o
ON DELETE CASCADEna chave estrangeira limpa automaticamente os anexos. Com JSONB, você precisa gerenciar a limpeza manualmente. - RLS individuais: cada anexo tem suas próprias regras de acesso. Você pode permitir a leitura de um anexo a um grupo sem dar acesso à tarefa inteira. Impossível com JSONB.
- Escalabilidade: adicionar metadados (dimensões da imagem, duração do vídeo, miniatura) é feito com um simples
ALTER TABLE ADD COLUMN. Com JSONB, você modifica um esquema implícito sem validação no lado do DB.
De acordo com as recomendações do PostgreSQL sobre JSONB, este tipo é ideal para dados semiestruturados cujo esquema é imprevisível. Os anexos têm um esquema perfeitamente previsível. A escolha foi clara.
Qual é a armadilha das URLs assinadas do Supabase Storage?
O Supabase Storage usa URLs assinadas para proteger o acesso a arquivos. Você não pode acessar o arquivo diretamente por meio de uma URL pública: você deve solicitar uma URL temporária, assinada com um token, que expira após um tempo configurável (por padrão, 1 hora).
Isso é excelente para a segurança. É um pesadelo para a UX se você não gerenciar corretamente. Imagine: um usuário abre seu feed, vê as imagens de suas tarefas. Ele deixa o aplicativo aberto por 2 horas. Ele rola: as imagens exibem erros 403. A URL expirou. A imagem ainda está lá no armazenamento, mas o link para acessá-la não é mais válido.
Minha solução: StorageService.refreshAttachmentUrlsBatch(). Este método recebe um array de storage_path (os caminhos permanentes no bucket Supabase) e regenera as URLs assinadas em lote. O ponto chave: nunca armazenamos a URL assinada como fonte da verdade. Armazenamos o storage_path e geramos a URL assinada sob demanda.
A atualização é acionada em três casos:
- Ao carregar uma lista: as URLs são geradas em lote para todos os anexos visíveis.
- Ao puxar para atualizar: o usuário força a atualização.
- Após retornar ao primeiro plano: se o aplicativo ficou em segundo plano por mais de uma hora, as URLs são regeneradas.
É o tipo de detalhe invisível quando funciona, e catastrófico quando falha. Eu havia enfrentado o mesmo tipo de desafio com o cache do feed e a gamificação: os dados existem, mas sua exibição depende de um mecanismo de atualização confiável.
Como a animação da estrela favorita foi projetada?
A animação da estrela é um bom exemplo de microinteração que faz a diferença. O princípio é simples: quando você toca na estrela, ela passa de vazia para cheia com um efeito de rebote.
Tecnicamente, é um Animated.spring com um toValue de 1.3 (overshoot de 30%) e depois um retorno a 1.0. O useNativeDriver: true garante que a animação seja executada na thread nativa, não na bridge JavaScript. É a mesma filosofia do botão nebulosa IA: as animações devem ser fluidas mesmo em dispositivos de baixo custo.
Adicionei um leve efeito háptico no iOS (via ReactNativeHapticFeedback) sincronizado com o pico do bounce. No Android, o feedback háptico é menos confiável dependendo dos fabricantes, então optei por uma mudança instantânea de cor: a estrela passa de cinza para dourado sem transição de cor, apenas o tamanho é animado.
A diferença entre um aplicativo amador e um aplicativo profissional muitas vezes está nesses detalhes. Cada interação deve dar um feedback. O usuário deve sentir que o aplicativo entendeu sua ação antes mesmo que o servidor confirme.
Como o FilterBar lida com a complexidade combinatória?
O FilterBar é provavelmente o componente mais subestimado do TAMSIV. Na superfície, é uma linha de botões. Por baixo, é um sistema de filtragem combinatória que gerencia 3 contextos (Privado / Compartilhado / Tudo) multiplicado por 3 modos (Tudo / Criadas por mim / Atribuídas a mim) multiplicado por N grupos com hierarquia.
O componente HierarchicalGroupPicker exibe a árvore de grupos com um toggle "incluir subgrupos". Quando você seleciona um grupo pai com a inclusão ativada, a consulta usa uma CTE recursiva para recuperar todos os IDs filhos. Eu estabeleci essa arquitetura no artigo sobre grupos hierárquicos.
O principal desafio é o desempenho. Cada mudança de filtro aciona uma nova consulta. Se o usuário tocar rapidamente em vários filtros, não queremos 5 consultas concorrentes. Implementei um debounce de 300ms: apenas o último estado do filtro aciona a consulta. O resultado é exibido via ContentCacheService que primeiro verifica o cache L1 na memória antes de buscar no DB.
Qual é o impacto desses "pequenos" recursos na retenção?
Os favoritos, a atribuição e os anexos não são recursos que fazem as pessoas baixarem um aplicativo. Ninguém procura por "aplicativo de tarefas com animação de estrela" na Play Store. Mas são recursos que fazem as pessoas manterem um aplicativo.
De acordo com uma análise da AppsFlyer sobre retenção móvel, a taxa média de retenção no dia 30 para aplicativos de produtividade é de 4,5%. Os aplicativos que se destacam são aqueles que reduzem o atrito nos fluxos de trabalho diários. Marcar uma tarefa como favorita com um toque, atribuir uma tarefa a um colega sem sair do contexto, ver a imagem anexada sem clicar em um link externo: isso é menos atrito.
Essa é a filosofia que eu já perseguia no artigo sobre pesquisa contextual e swipe: cada interação economizada é um ponto de atrito eliminado. E na produtividade, o atrito é o inimigo número 1 da adoção.
Perguntas frequentes
É possível filtrar as tarefas favoritas na agenda?
Sim. O filtro favorito está disponível no feed e na agenda. As tarefas marcadas como favoritas aparecem com o ícone de estrela em todas as visualizações onde são exibidas, incluindo o calendário com seus filtros avançados.
Quantas pessoas podem ser atribuídas a uma tarefa?
Não há limite técnico. A tabela de junção collaborative.task_assignments permite quantas atribuições forem necessárias. Na prática, atribuir mais de 5 pessoas à mesma tarefa torna-se difícil de acompanhar, mas o sistema suporta isso.
Os anexos têm limite de tamanho?
Sim. O plano Free permite arquivos de até 5 MB. O plano Pro aumenta para 25 MB. O plano Team para 50 MB. Esses limites são gerenciados no frontend antes do upload e no backend por meio das políticas do Supabase Storage.
O que acontece se uma URL assinada expirar durante a visualização?
O StorageService detecta erros 403 e aciona automaticamente uma atualização da URL. O usuário vê um breve placeholder de carregamento e a imagem reaparece. O processo é transparente.
Os favoritos são sincronizados entre o celular e a web?
Sim, em tempo real. O ContentCacheService usa os canais Supabase Realtime para propagar as mudanças de favoritos entre todos os dispositivos conectados à mesma conta.