Saltar al contenido principal

Perfil de Usuario — Referencia Técnica

← Volver a Perfil de Usuario

Dónde vive esto

Backend

Frontend

Checklist de implementación técnica

  • me — resolver conectado en user-profile.resolver.js; mostrado por ProfilePage.tsx
  • updateProfile — mutation conectada; handleSave de EditProfilePage.tsx la invoca en el botón "Guardar cambios" (este documento afirmaba antes que no existía un controlador onClick — corregido)
  • userByUsername — resolver conectado; mostrado por PublicProfilePage.tsx
  • Insignia isVerified — se muestra junto al nombre de usuario cuando isVerified es verdadero, tanto en ProfilePage.tsx como en PublicProfilePage.tsx
  • Enlaces sociales (addSocialLink / updateSocialLink / deleteSocialLink / mySocialLinks / userSocialLinks) — ahora en funcionamiento de extremo a extremo: social-link.resolver.js + social-link.type.js exponen el esquema sobre social-link.manager.js / social-link.access-service.js, y SocialLinksPage.tsx / PublicProfilePage.tsx lo consumen (este documento afirmaba antes que no existía exposición de GraphQL — corregido)
  • updatePrivacySettings — mutation conectada; AccountPrivacyPage.tsx la llama mediante useMutation con controladores reales para los interruptores (este documento afirmaba antes que eran elementos estáticos de relleno — corregido; la página se desarrolló sustancialmente durante la revisión de Configuración, ver Configuración)
  • followUser / unfollowUser — mutations conectadas; el botón de seguir de PublicProfilePage.tsx llama a handleFollowClick, que las invoca (este documento afirmaba antes que no existía un controlador onClick — corregido)
  • Visitantes del perfil — PublicProfilePage.tsx dispara recordProfileVisit al montar; ProfileViewersPage.tsx lista myProfileViewers; el interruptor hideProfileVisits en AccountPrivacyPage.tsx hace que recordVisit sea un no-op para las visitas de ese usuario a otros perfiles

Campos del modelo User

Identidad

CampoDescripción
usernameNombre de usuario único
firstName / lastNameNombre completo
bioBiografía del perfil
profilePictureURL de la foto de perfil
coverPhotoURL de la foto de portada
dateOfBirth / genderInformación demográfica
accountTypeTipo de cuenta (personal, creator, etc.)

Estado de la cuenta

CampoDescripción
accountStatusEstado actual de la cuenta
isVerified / verificationStatusInsignia de verificación
verificationCategoryCategoría de la insignia (por ejemplo, creator, business)
isPrivateCuenta privada (requiere aprobación de seguidor)
isEmailVerified / isPhoneVerifiedVerificación de contacto
isOnboardingCompleted / onboardingStepProgreso de incorporación

Actividad

CampoDescripción
isOnlineActualmente en línea
lastActiveAtMarca de tiempo de la última vez visto
lastPostAtFecha de la última publicación
engagementScorePuntaje de interacción calculado
locale / timezoneConfiguración regional

Seguridad

CampoDescripción
twoFactorEnabled2FA habilitado
securityNotificationsEnabledAlertas de seguridad activas
isTermsConditionsAcceptedTérminos y condiciones aceptados

Restricciones y suspensión

CampoDescripción
suspensionCount / suspendedAtHistorial de suspensiones
isRestricted / restrictionTypesRestricciones activas
tagApprovalRequiredDebe aprobar etiquetas antes de que aparezcan
autoRemoveTagsElimina automáticamente las etiquetas del contenido

Modo pausa

Permite pausar temporalmente la cuenta:

CampoDescripción
isOnBreakActualmente en pausa
breakStartedAt / breakEndsAtVentana de la pausa
breakDurationDaysDuración planificada
scheduledBreakStartsAtPausa programada futura
autoReplyMessageRespuesta automática enviada a cualquiera que envíe un mensaje durante la pausa

Memorialización

Para cuentas de usuarios fallecidos:

CampoDescripción
isMemorialized / memorializedAtEstado de memorialización
memorializationStatusEstado del proceso
legacyContactIdContacto de legado designado
allowTributesPermitir publicaciones de tributo
allowLegacyContactPostsEl contacto de legado puede publicar
showMemorialBannerMostrar el banner de memorial en el perfil

Consultas de perfil

me devuelve al usuario autenticado completo — se usa al iniciar la app para hidratar el contexto del usuario actual. user y userByUsername obtienen datos de perfil público de cualquier cuenta. searchUsers es el autocompletado de la barra de búsqueda. suggestedUsers devuelve recomendaciones de seguimiento personalizadas para la pantalla de inicio.

query Me { me { id username bio profilePicture coverPhoto isVerified isPrivate twoFactorEnabled } }
query User($id: ID!) { user(id: $id) { id username } }
query UserByUsername($username: String!) { userByUsername(username: $username) { id username bio isVerified } }
query SearchUsers($query: String!, $limit: Int) { searchUsers(query: $query, limit: $limit) { id username profilePicture } }
query SuggestedUsers($limit: Int) { suggestedUsers(limit: $limit) { id username profilePicture } }

Disponibilidad de nombre de usuario / correo electrónico

Estas dos consultas se llaman en tiempo real mientras el usuario escribe durante el registro o la edición de perfil. isUsernameAvailable devuelve un message explicando por qué el nombre de usuario está en uso (por ejemplo, "Ya está en uso").

query CheckUsername($username: String!) { isUsernameAvailable(username: $username) { available message } }
query CheckEmail($email: String!) { isEmailAvailable(email: $email) { available message } }

Estadísticas y relación

userStats devuelve contadores (seguidores, seguidos, amigos cercanos) y las banderas de relación entre quien llama y el usuario objetivo — se usa para renderizar el estado del botón seguir/mensaje en un perfil.

relationshipStatus es un objeto de relación más detallado que también incluye canMessage y canViewPosts — se usa al renderizar un perfil para saber si mostrar el botón "Mensaje" y si las publicaciones son accesibles.

query UserStats($userId: ID!) {
userStats(userId: $userId) {
followersCount followingCount closeFriendsCount
isFollowing isFollower isCloseFriend isBlocked isBlockedBy hasPendingRequest
}
}

query RelationshipStatus($targetUserId: ID!) {
relationshipStatus(targetUserId: $targetUserId) {
isSelf isFollowing isFollower isBlocked isMuted isCloseFriend
hasPendingRequest isMutualFollowing canMessage canViewPosts
}
}

Acciones sociales

followUser siempre crea la relación de seguimiento de inmediato (no verifica isPrivate) — está pensado para llamantes que ya saben que el objetivo es público, por ejemplo, el botón de seguimiento rápido de SuggestedUsers.tsx. sendFollowRequest es el punto de entrada que sí tiene en cuenta las cuentas privadas: internamente sigue de inmediato si el objetivo es público, o crea una solicitud pendiente si es privado, y su respuesta incluye requestRequired: true en este último caso. handleFollowClick de PublicProfilePage.tsx elige entre ambas usando el user.isPrivate ya cargado.

Nota: el manager de sendFollowRequest en realidad devuelve claves en snake_case (request_required/request_id), mientras que el esquema declara requestRequired en camelCase y no nulo — seleccionarlo provoca un error "Cannot return null for non-nullable field", por lo que PublicProfilePage.tsx deliberadamente solo selecciona success/message/requestId y depende de un refetch de relationshipStatus para conocer el estado real.

unfollowUser elimina la relación de seguimiento. blockUser también deja de seguir si existía un seguimiento. muteUser oculta el contenido del usuario de tu feed sin eliminar la relación de seguimiento. removeFollower elimina a alguien de tus seguidores sin bloquearlo.

mutation FollowUser($userId: ID!) { followUser(userId: $userId) { success } }
mutation UnfollowUser($userId: ID!) { unfollowUser(userId: $userId) { success } }
mutation BlockUser($userId: ID!) { blockUser(userId: $userId) { success } }
mutation MuteUser($userId: ID!) { muteUser(userId: $userId) { success } }
mutation RemoveFollower($id: ID!) { removeFollower(followerId: $id) { success } }

# Follow requests (private accounts) - do not select requestRequired, see note above
mutation SendRequest($userId: ID!) { sendFollowRequest(userId: $userId) { success requestId } }
mutation AcceptRequest($id: ID!) { acceptFollowRequest(requestId: $id) { success } }
mutation RejectRequest($id: ID!) { rejectFollowRequest(requestId: $id) { success } }
mutation CancelRequest($id: ID!) { cancelFollowRequest(requestId: $id) { success } }

# Close Friends
mutation AddCloseFriend($userId: ID!) { addToCloseFriends(userId: $userId) { success } }
mutation RemoveCloseFriend($userId: ID!) { removeFromCloseFriends(userId: $userId) { success } }

# Favorites (reorderable, separate from following)
mutation AddFavorite($userId: ID!) { addToFavorites(userId: $userId) { favorite { position } } }
mutation RemoveFavorite($userId: ID!) { removeFromFavorites(userId: $userId) { success } }
mutation ReorderFavorites($ids: [ID!]!) { reorderFavorites(userIds: $ids) { success } }

Operaciones masivas

Estas mutaciones procesan una lista de IDs de usuario en una sola llamada a la API. successCount y failedCount permiten al cliente mostrar un toast resumen sin verificar cada resultado individualmente.

mutation BulkFollow($ids: [ID!]!) { bulkFollowUsers(userIds: $ids) { successCount failedCount } }
mutation BulkUnfollow($ids: [ID!]!) { bulkUnfollowUsers(userIds: $ids) { successCount } }
mutation BulkBlock($ids: [ID!]!) { bulkBlockUsers(userIds: $ids) { processedCount } }
mutation BulkRemoveFollowers($ids: [ID!]!) { bulkRemoveFollowers(followerIds: $ids) { processedCount } }
mutation BulkProcessRequests($ids: [ID!]!, $action: String!) {
bulkProcessFollowRequests(requestIds: $ids, action: $action) { processedCount }
}
mutation BulkMute($ids: [ID!]!) { bulkMuteUsers(userIds: $ids) { success message results { success failed } } }

Permisos de etiquetas

Controla quién puede etiquetarte en publicaciones y si las etiquetas requieren tu aprobación antes de aparecer. requireApproval: true significa que las etiquetas quedan en cola para revisión. friendsOnly: true limita el etiquetado a seguidores mutuos. disabled: true impide todo etiquetado.

removeTag elimina una etiqueta existente de una de tus propias publicaciones y devuelve un Boolean. Aprobar o rechazar una etiqueta pendiente (una que quedó en cola porque requireApproval está activado) es un flujo aparte mediante pendingTagReviews/approvePostTag/rejectPostTag (más abajo); se planeó un par approveTag/rejectTag con el mismo nombre para este sistema genérico de permisos, pero nunca se conectó (updateTagPermissions y removeTag son las únicas mutaciones activas aquí).

type TagPermissions {
canBeTagged: Boolean!
requireApproval: Boolean!
friendsOnly: Boolean!
disabled: Boolean!
}

query TagPermissions { tagPermissions { canBeTagged requireApproval friendsOnly disabled } }

# input is a JSON blob (no dedicated TagPermissionsInput type), shaped like TagPermissions
mutation UpdateTagPermissions($input: JSON!) {
updateTagPermissions(input: $input) { canBeTagged requireApproval friendsOnly disabled }
}

mutation RemoveTag($tagId: ID!) { removeTag(tagId: $tagId) }

Revisar las etiquetas que otras personas te pusieron (a diferencia de editar tus propios permisos) es un par de mutaciones separado, expuesto por tag-review.resolver.js:

query PendingTagReviews { pendingTagReviews { id user { id username } media { id } positionX positionY createdAt } }

mutation ApprovePostTag($mentionId: ID!) { approvePostTag(mentionId: $mentionId) }
mutation RejectPostTag($mentionId: ID!) { rejectPostTag(mentionId: $mentionId) }

Intereses

Los usuarios seleccionan intereses durante la incorporación (completeOnboarding) o más tarde desde la configuración de su perfil. Los intereses potencian interestSuggestions y otros rankings de recomendación. A diferencia de un sistema respaldado por una taxonomía aparte, aquí los intereses son simplemente un arreglo de cadenas de texto libre almacenadas en User (columna interestTags, expuesta como User.interests) — no existe un tipo Interest con id/slug/icono en este flujo de GraphQL. updateInterests reemplaza toda la selección del usuario de una sola vez (máximo 50 entradas, recortadas y filtradas por el resolver).

query MyInterests { me { interests } }

mutation UpdateInterests($interests: [String!]!) {
updateInterests(interests: $interests) { id interests }
}

query InterestSuggestions($limit: Int, $interests: [String!]) {
interestSuggestions(limit: $limit, interests: $interests) { id username profilePicture }
}

Enlaces sociales

Los usuarios pueden vincular sus perfiles en otras plataformas. platform es una cadena como twitter, instagram o website. Estos enlaces se muestran en la tarjeta de perfil propio del usuario (SocialLinksPage.tsx en Configuración → Enlaces) y en los perfiles públicos (PublicProfilePage.tsx mediante userSocialLinks).

mutation AddSocialLink($input: CreateSocialLinkInput!) {
addSocialLink(input: $input) { id platform url title clickCount }
}

mutation UpdateSocialLink($id: ID!, $input: UpdateSocialLinkInput!) {
updateSocialLink(id: $id, input: $input) { id platform url isActive visibility }
}

mutation DeleteSocialLink($id: ID!) { deleteSocialLink(id: $id) }
mutation ReorderSocialLinks($ids: [ID!]!) { reorderSocialLinks(ids: $ids) }
mutation TrackSocialLinkClick($id: ID!) { trackSocialLinkClick(id: $id) { id clickCount } }

query MySocialLinks { mySocialLinks { id platform url title displayOrder clickCount } }
query UserSocialLinks($userId: ID!) { userSocialLinks(userId: $userId) { id platform url title } }
query SocialLinkStats($userId: ID) { socialLinkStats(userId: $userId) { totalLinks totalClicks visibleLinks hiddenLinks platforms } }

Apelaciones de restricción

Si una cuenta está suspendida o baneada (accountStatus), el usuario puede presentar una apelación contra ella mediante appealRestriction, siempre que no tenga ya una pendiente. myLatestAppeal devuelve la apelación más reciente del llamante, de cualquier estado (o null si nunca ha apelado); getAppealStatus busca una apelación específica por id, y el llamante debe ser su propietario.

mutation AppealRestriction($reason: String!) {
appealRestriction(reason: $reason) { id status submittedAt reason }
}

query GetAppealStatus($appealId: ID!) {
getAppealStatus(appealId: $appealId) { id status reason submittedAt reviewedAt decisionNotes }
}

query MyLatestAppeal { myLatestAppeal { id status reason submittedAt reviewedAt decisionNotes } }

Usuarios en tendencia

getTrendingUsers devuelve cuentas con un crecimiento rápido en su número de seguidores, ordenadas por trendingScore. trendingReason es una etiqueta legible para humanos como "Publicación viral" o "Cuenta verificada nueva" — se muestra como subetiqueta en la tarjeta de tendencia. El manager en sí devuelve un envoltorio { trendingUsers, timeframe, category, pagination }, que el resolver desenvuelve a la lista [TrendingUser!]! que declara el esquema.

query TrendingUsers($limit: Int, $offset: Int, $timeframe: String, $category: String) {
getTrendingUsers(limit: $limit, offset: $offset, timeframe: $timeframe, category: $category) {
user { id username profilePicture isVerified followersCount }
trendingScore growthRate trendingReason
}
}

Usuarios en línea

getOnlineUsers devuelve las cuentas que están actualmente en línea — útil para un widget de "Quién está en línea". lastSeen es la marca de tiempo del último latido (heartbeat) del usuario.

query OnlineUsers($limit: Int, $offset: Int) {
getOnlineUsers(limit: $limit, offset: $offset) {
user { id username profilePicture }
lastSeen isCurrentlyOnline activityStatus
}
}

Editar perfil

updateProfile actualiza parcialmente el perfil del usuario autenticado. Solo se actualizan los campos pasados en input.

mutation UpdateProfile($input: UserProfileUpdateInput!) {
updateProfile(input: $input) { id username bio profilePicture coverPhoto }
}

Gestión de cuenta

mutation DeactivateAccount($password: String!) { deactivateAccount(password: $password) { success } }
mutation ReactivateAccount($password: String!) { reactivateAccount(password: $password) { success } }
mutation RequestDeletion($password: String!) { requestAccountDeletion(password: $password) { success } }
mutation CancelDeletion { cancelAccountDeletion { success } }
mutation DeleteAccount($password: String!) { deleteAccount(password: $password) { success } }

# Skips the grace period requestAccountDeletion schedules and deletes right away
mutation DeleteAccountImmediately($password: String!) { deleteAccountImmediately(password: $password) { success } }

Visitantes del perfil

Cada vez que un usuario abre el perfil público de otra persona, el frontend dispara recordProfileVisit en segundo plano. El resolver nunca lanza un error al llamante — un fallo se silencia y devuelve false — de modo que un problema de registro nunca bloquea la carga del perfil. Las visitas se registran en un log de solo escritura (user_profile_view, modelado a partir de user_post_view) y se eliminan duplicados: una visita repetida del mismo visitante dentro de 60 segundos no crea una nueva fila, y getMyViewers además elimina duplicados por visitante, de modo que alguien que ha visitado muchas veces solo aparece una vez, con la hora de su visita más reciente.

Las visitas a tu propio perfil nunca se registran. Si el visitante tiene activado hideProfileVisits (ver Configuración de privacidad), recordVisit no hace nada silenciosamente — la lista del usuario visitado simplemente no incluirá a ese visitante, sin ningún error visible para ninguna de las dos partes.

mutation RecordProfileVisit($targetUserId: ID!) { recordProfileVisit(targetUserId: $targetUserId) }

query MyProfileViewers($limit: Int, $offset: Int) {
myProfileViewers(limit: $limit, offset: $offset) {
viewer { id username profilePicture isVerified }
viewedAt
}
}

query MyProfileViewerCount { myProfileViewerCount }

mutation ClearMyProfileViewers { clearMyProfileViewers }