Saltar al contenido principal

Publicaciones y Feed — Referencia técnica

← Volver a Publicaciones y Feed

Dónde vive esto

Backend

Frontend

Checklist de implementación técnica

  • feed — resolver conectado en post.resolver.js; se llama desde HomePage.tsx vía useQuery de Apollo
  • reactToComment(commentId, reaction) / removeCommentReaction + PostComment.viewerReaction / reactionCounts — reacciones a comentarios (like/love/haha/wow/sad/angry). Se corrigió un error de persistencia: post-comment-interaction.access-service.js consultaba comment_id en lugar de post_comment_id
  • likePost / unlikePost — resolvers conectados en post-interaction.resolver.js; se llaman desde handleLike de PostCard.tsx (~línea 499)
  • createComment — resolver conectado en post-comment.resolver.js; se llama desde PostModal.tsx y stories/StoryViewer.tsx
  • repostPost / undoRepost — resolvers conectados en post.resolver.js; se llaman desde handleRepost de PostCard.tsx (~línea 566) y handleRepost de PostModal.tsx (~línea 710), con una verificación hasUserReposted
  • sharePost — no existe ninguna mutation de GraphQL con ese nombre en resolvers/schema. El ícono de "compartir" en PostCard.tsx en realidad ejecuta la acción de repost; existe por separado una función nativa real (no GraphQL) de compartir/copiar enlace en PostOptionsMenu.tsx (navigator.share / copiar enlace, sin necesidad de llamada al backend)
  • createPost — resolver conectado en post.resolver.js; se llama desde CreatePostModal.tsx, que gestiona la carga de contenido multimedia, la extracción de hashtags y el envío de usuarios etiquetados
  • taggedUsers (PostMention a través de post_mentions) — handleImageTapForTag de CreatePostModal.tsx (~línea 751) permite al usuario tocar un punto en una imagen subida, buscar un usuario (SEARCH_USERS_FOR_TAG), y los pines se envían como taggedUsers en createPost (~línea 907); post.resolver.js:478 los expone de vuelta en el tipo Post. PostTagInput.mediaIndex/x/y ahora son opcionales, por lo que un usuario también puede ser etiquetado sin un pin en la foto (por ejemplo, en una publicación de solo texto) — esas etiquetas "sin posición" se combinan con las etiquetadas por pin antes del envío.
  • Extracción automática de hashtags — post.manager.js:217 llama a hashtagManager.extractAndCreateHashtags en cada createPost, lo que alimenta el feed /hashtag/[tag] (ver Hashtags y Tendencias)
  • feed (reutilizada para la pestaña "Descubrir") — la query DISCOVER_POSTS de ExplorePage.tsx usa un alias del campo feed
  • trendingPosts — se agregó lógica de ranking real en post.access-service.js#getTrendingPosts (publicaciones públicas recientes puntuadas por viewsCount*1 + likesCount*3 + commentsCount*4 + sharesCount*5 + savesCount*4 en una ventana de 48h por defecto) y se expuso a través de post.resolver.js; conectada en la pestaña "En tendencia" de ExplorePage.tsx. Antes era solo un comentario de documentación (@method getTrendingPosts) sin implementación en ningún lugar y sin campo alguno en el schema.
  • Post.viewerHasLiked / viewerHasSaved / viewerHasReposted / interactionCount / giftStats — resueltos del lado del servidor como resolvers de campo de Post en post.resolver.js, incluidos en cualquier query que ya cargue la publicación (feed, userPosts, post). La query del feed de HomePage.tsx los selecciona, y PostCard.tsx los lee directamente de la publicación (su verificación inlineState) en lugar de disparar queries separadas de hasUserLikedPost / isPostSaved / hasUserReposted / postInteractionCounts por cada tarjeta — esas queries por publicación se mantienen solo como respaldo para contextos que no precargan este estado (por ejemplo, PostModal.tsx).
  • ContentTagManager / UserContentInterest — la lógica existe en content-tag.manager.js, pero no es requerida por post.manager.js ni por ningún resolver — código huérfano, sin exposición en el schema ni uso en el frontend

Modelo Post

CampoTipoDescripción
textStringTexto de la publicación
visibilityEnumpublic / followers / private / subscribers
locationStringEtiqueta de ubicación opcional
media[PostMedia]Imágenes o videos adjuntos
viewsCountIntConteo acumulado de vistas
sharesCountIntConteo de veces compartida
isPublishedBooleanBorrador vs. publicada

Contenido multimedia (PostMedia)

Cada elemento multimedia tiene: mediaUrl, mediaType, thumbnailUrl, duration, width, height, aspectRatio, order. El campo order controla el orden de visualización en publicaciones con múltiples elementos multimedia. filterCss incluye un filtro estilo Instagram (por ejemplo, contrast(1.2) saturate(1.35)) elegido al momento de publicar, aplicado sobre la imagen original al momento de mostrarla. objectFit (cover/contain) controla cómo la imagen llena su marco. isPreview marca un elemento como vista previa gratuita en una publicación paga — visible para todos incluso antes de la compra, mientras el resto permanece bloqueado.

Queries

feed devuelve el feed principal personalizado del usuario autenticado — publicaciones de las cuentas que sigue, ordenadas por relevancia y actualidad. Admite paginación basada en cursor mediante limit y offset. Una publicación que el usuario haya publicado él mismo en los últimos 5 minutos se fija en la parte superior de su propio feed (post.access-service.js#getFeed), de modo que una publicación recién creada aparece de inmediato en lugar de esperar a ganar puntaje de interacción/actualidad.

userPosts obtiene todas las publicaciones publicadas de un usuario específico. Útil para renderizar la cuadrícula de un perfil. Respeta la configuración de privacidad del usuario objetivo — las cuentas privadas solo devuelven publicaciones si quien llama es seguidor.

postStats devuelve contadores agregados de una publicación sin obtener el objeto completo. Úsalo para actualizar estadísticas de forma ligera sin volver a cargar el contenido multimedia ni los comentarios.

query GetFeed($limit: Int, $offset: Int) {
feed(limit: $limit, offset: $offset) {
id text visibility location viewsCount sharesCount
# Bundled per-viewer state - lets the card skip a separate query per post
viewerHasLiked viewerHasSaved viewerHasReposted interactionCount
user { id username profilePicture }
media { mediaUrl mediaType thumbnailUrl aspectRatio }
comments { id text user { username } }
interactions { reactionType user { username } }
}
}

# Recent public posts ranked by engagement - not personalized, unlike feed
query GetTrendingPosts($limit: Int, $offset: Int, $windowHours: Int) {
trendingPosts(limit: $limit, offset: $offset, windowHours: $windowHours) {
id text viewsCount likesCount commentsCount sharesCount
user { id username profilePicture }
media { mediaUrl mediaType thumbnailUrl }
}
}

# All posts from a specific user's profile
query UserPosts($userId: ID!, $limit: Int, $offset: Int) {
userPosts(userId: $userId, limit: $limit, offset: $offset) { id text media { mediaUrl mediaType } }
}

# Lightweight counters for a post
query PostStats($postId: ID!) {
postStats(postId: $postId) {
viewsCount likesCount commentsCount sharesCount
}
}

Mutations

createPost crea una nueva publicación. Si isPublished es false la publicación se guarda como borrador y no se muestra en los feeds. El contenido multimedia se puede pasar como mediaUrls (arreglo simple de URLs) o mediaItems (URL + dimensiones).

updatePost permite editar el texto, la visibilidad o la ubicación de una publicación existente. Solo el autor de la publicación puede actualizarla.

deletePost elimina permanentemente la publicación y todo su contenido multimedia, comentarios y reacciones asociados.

incrementPostViews debe llamarse una vez por vista única — típicamente cuando la publicación entra en el viewport. El servidor deduplica las llamadas rápidas.

mutation CreatePost($input: PostCreateInput!) {
createPost(input: $input) { id text visibility isPublished }
}

mutation UpdatePost($postId: ID!, $input: PostUpdateInput!) {
updatePost(postId: $postId, input: $input) { id text visibility }
}

# Permanently deletes post and all associated data
mutation DeletePost($postId: ID!) { deletePost(postId: $postId) }

# Call once when a post enters the user's viewport
mutation IncrementViews($postId: ID!) { incrementPostViews(postId: $postId) { viewsCount } }

Reacciones (6 tipos)

Los usuarios pueden reaccionar a una publicación con una de 6 emociones. Llamar a likePost nuevamente con un interactionType distinto reemplaza la reacción anterior — un usuario solo puede tener una reacción por publicación a la vez.

enum InteractionType { like love haha wow sad angry }

# Add or change your reaction to a post
mutation LikePost($postId: ID!, $type: InteractionType) {
likePost(postId: $postId, interactionType: $type) { id reactionType }
}

# Remove your reaction entirely
mutation UnlikePost($postId: ID!) { unlikePost(postId: $postId) }

# Get a breakdown of reactions by type
query InteractionCounts($postId: ID!) {
postInteractionCounts(postId: $postId) {
like love haha wow sad angry total
}
}

# Check if the current user has already reacted
query HasUserLiked($postId: ID!) { hasUserLikedPost(postId: $postId) }

Comentarios (anidados)

Los comentarios admiten un nivel de anidación mediante parentId — pasa un parentId para crear una respuesta a un comentario existente. Omítelo para crear un comentario de nivel superior.

postComments devuelve los comentarios de nivel superior con sus respuestas precargadas. Para publicaciones con muchos comentarios, usa commentReplies para cargar las respuestas de forma diferida bajo demanda.

# Create a comment; pass parentId to reply to an existing comment
mutation CreateComment($input: PostCommentCreateInput!) {
createComment(input: $input) { id text parentId user { username } }
}

mutation UpdateComment($commentId: ID!, $text: String!) {
updateComment(commentId: $commentId, input: { text: $text }) { id text }
}

mutation DeleteComment($commentId: ID!) { deleteComment(commentId: $commentId) }

# Top-level comments with nested replies
query PostComments($postId: ID!, $limit: Int, $offset: Int) {
postComments(postId: $postId, limit: $limit, offset: $offset) {
id text
user { username profilePicture }
replies { id text user { username } }
}
}

# Lazy-load replies for a specific comment
query CommentReplies($commentId: ID!, $limit: Int, $offset: Int) {
commentReplies(commentId: $commentId, limit: $limit, offset: $offset) { id text user { username } }
}

# Cheap counter — use before fetching comments to decide whether to show the section
query CommentCount($postId: ID!) { postCommentCount(postId: $postId) }

Reacciones a comentarios

Los comentarios admiten los mismos 6 tipos de reacción que las publicaciones. Cada usuario puede tener como máximo una reacción por comentario.

# React to a comment (replaces any previous reaction); returns a plain Boolean
mutation ReactToComment($commentId: ID!, $reaction: String!) {
reactToComment(commentId: $commentId, reaction: $reaction)
}

# Remove your reaction from a comment; also a plain Boolean
mutation RemoveCommentReaction($commentId: ID!) {
removeCommentReaction(commentId: $commentId)
}

# There is no standalone "commentReactions" query - per-type counts and the
# viewer's own reaction come back as fields on the comment itself.
query CommentWithReactions($id: ID!) {
postComment(id: $id) {
id
reactionCounts
viewerReaction
}
}

Etiquetas de contenido y personalización del feed

El backend clasifica automáticamente las publicaciones con etiquetas ContentTag usando Google Cloud Vision / Video Intelligence. Estas etiquetas impulsan el motor de personalización del feed.

UserContentInterest registra señales de interacción por etiqueta para cada usuario: postsCreated, postsLiked, postsCommented, postsShared, postsSaved, postsViewed, timeSpentSeconds. A partir de estas señales, el sistema deriva un interestScore, un affinityLevel (low / medium / high), y un indicador isTrendingInterest. Las publicaciones cuyas etiquetas coinciden con los intereses de alta afinidad de un usuario tienen mayor prioridad en su feed.

Componentes de frontend

ComponenteDescripción
PostCard.tsxRenderiza una sola publicación en el feed
CreatePostModal.tsxModal para crear una publicación con carga de contenido multimedia
SuggestedUsers.tsxPanel lateral con sugerencias de cuentas a seguir
page-components/HomePage.tsxPágina principal del feed