Mensajes y conversaciones — Referencia técnica
← Volver a Mensajes y conversaciones
Dónde vive esto
Backend
apps/backend/graphql/resolvers/message.resolver.js—sendMessage,conversationMessages, mutaciones de fijar/destacar/reaccionar/reenviar/responder/expirar/ver una vez/ubicación en vivo/encuesta/programar/borrador, además de las suscripcionesmessageAdded/messageUpdated/messageDeleted/messageRead/messageReactionAdded/typingIndicatorapps/backend/graphql/resolvers/message-purchase.resolver.js—lockMessage,unlockMessage,purchaseMessage,refundMessagePurchaseapps/backend/graphql/resolvers/conversation.resolver.js—myConversations,createConversation, silenciar/archivar/fijar/proteger con candado/bloquear,setTypingStatus, configuración de conversaciónapps/backend/graphql/resolvers/conversation-subscription.resolver.js— conversaciones con acceso restringido por suscripción (enableConversationSubscription,subscribeToConversation, etc.)apps/backend/managers/message-managers/message.manager.js— lógica de negocio central de los mensajesapps/backend/managers/message-managers/conversation.manager.js— ciclo de vida de la conversación; también publica el evento del indicador de "escribiendo" a través depubsub.service.jsapps/backend/data-access-services/message/message.access-service.js— acceso a la base de datos de mensajes (incluyendo reacciones, destacados y fijados)apps/backend/graphql/resolvers/message-translation.resolver.js/apps/backend/graphql/types/message-translation.type.js— consultasmessageTranslations/myTranslationSettingy mutaciónsetMessageTranslationapps/backend/managers/message-managers/message-translation.manager.js/apps/backend/services/message-translation.service.js— lógica de negocio y caché de traducción, y el motor de traducción independiente del proveedorapps/backend/managers/message-managers/conversation-invite.manager.js— sustentaconversationInvitePreview/joinConversationViaInvite(ambas resueltas enconversation.resolver.js)apps/backend/services/message-notification.service.js— envía notificaciones push/dentro de la app cuando se envía un mensajeapps/backend/services/chat-cache.service.js— caché de chat respaldada por Redis, usada pormessage-notification.service.js- Nota:
services/typing-indicator.service.jsexiste en el código base pero actualmente no se requiere en ningún lugar — el indicador en vivo de "escribiendo" en realidad es impulsado porconversation.manager.js, que publica a través depubsub.service.js, y no por ese archivo. Además, los access-servicesmessage-reaction/message-star/message-viewexisten pero ningún manager los invoca; las reacciones, los destacados y los fijados pasan directamente pormessage.access-service.js.
Frontend
apps/frontend-nextjs/src/page-components/MessagesPage.tsx— estructura de la página de mensajesapps/frontend-nextjs/src/app/direct/t/[threadId]/page.tsx— ruta de un hilo de conversación individualapps/frontend-nextjs/src/components/chat/ChatView.tsx— ventana principal de chatapps/frontend-nextjs/src/components/chat/components/MessageList.tsx— renderizado de la lista de mensajesapps/frontend-nextjs/src/components/chat/components/MessageInputArea.tsx— compositor / campo de envíoapps/frontend-nextjs/src/components/chat/ConversationList.tsx— lista de conversaciones/bandeja de entrada
Checklist de implementación técnica
-
sendMessage/conversationMessages— resolvers conectados enmessage.resolver.js;ChatView.tsxenvía y recibe mensajes mediantecreateConversationcontype: 'direct' -
setGroupPostingPolicy/setParticipantPostingPermission— permisos de publicación en grupos; aplicados enmessage.manager.js#createMessage;Conversation.postingPolicy/viewerCanSend,ConversationParticipant.canSendMessages(migración20260721100000) -
conversationRemovalPreview(conversationId, userId)+ reembolso enremoveParticipant— reembolso prorrateado del período no utilizado para miembros de pago (conversation-subscription.manager.js#refundSubscription/computeProratedRefund) -
User.canAddToGroup— campo relativo al usuario que consulta, usado para filtrar la búsqueda de agregar miembros (refleja la verificación de privacidadadd_to_group) -
messagesByMediaType(mediaType: link)— pestaña de enlaces; se agrególinkal enumMessageType(coincide con mensajes de texto que contienen una URL). Destacados/Fijados se muestran como secciones separadas enSharedContentSection.tsx -
createConversation/addParticipant/updateParticipantRole/transferAdmin— resolvers conectados enconversation.resolver.js;ConversationList.tsxtiene un botón "Crear grupo" (New group) que llama acreateGroupConversationcontype: 'group'(este documento antes decía que no existía interfaz para crear grupos — corregido) - Enlaces de invitación de grupo para compartir —
conversationInvitePreview(query) yjoinConversationViaInvite(mutation) enconversation.resolver.js, respaldados porconversation-invite.manager.js, consumidos por una página de destino:apps/frontend-nextjs/src/app/join/[token]/page.tsx→JoinConversationPage.tsx, que muestra una vista previa del grupo (nombre, avatar, cantidad de miembros) y requiere iniciar sesión antes de unirse. - mutation
setTypingStatus+ subscriptiontypingIndicator— conectadas enChatView.tsxmedianteuseSubscription(TypingIndicatorDocument)y una llamada asetTypingStatus - query
messageReadReceipts+ subscriptionmessageRead, marcado automático víaconversationManager.markAsRead/messageManager.markConversationMessagesReadcuando se obtieneconversationMessages—useChatMessages.tsse suscribe amessageRead(declarada en línea comoMESSAGE_READ_SUB, ya que codegen no ha generado un documento para ella) y cambia elstatusdel mensaje correspondiente areaden la caché de Apollo;MessageBubble.tsxrenderiza el doble check azul cuandostatus === 'read' -
addReaction/removeReaction— resolvers conectados enmessage.resolver.js; se llaman desdeuseChatActions.ts,MessageContextMenu.tsx,MessageBubble.tsx; la subscriptionmessageReactionAdded(también declarada en línea enuseChatMessages.ts, ya que tampoco está en las operaciones generadas) vuelve a obtenermessageReactionspara el mensaje afectado, de modo que las reacciones de otros participantes aparecen en vivo -
pinMessage/unpinMessage/starMessage/unstarMessage— conectados enChatView.tsx,MessageContextMenu.tsx,useChatActions.ts, renderizados mediantePinnedMessagesSection.tsx - subscription
messageAdded— consumida enuseChatMessages.ts/ChatView.tsxpara agregar mensajes entrantes en vivo -
isPaid/price(definidos al enviar) +purchaseMessagepara desbloquear — ambos conectados enChatView.tsxy renderizados mediantePaidMediaModal.tsx/MessageBubble.tsx -
startCall— mutation encall.resolver.js;VoiceCallModal.tsxse lanza desdeChatView.tsx -
sendCoinsViaMessage— resolver conectado enmessage.resolver.js; se llama en el manejador de envío deChatView.tsxcuando haypendingCoins -
createPoll/voteInPoll— conectados enChatView.tsxmediantePollCreationModal.tsx -
setMessageExpiration— interruptor de desaparición por mensaje; conectado medianteuseChatActions.ts#handleSetExpirationy ofrecido desdeMessageContextMenu.tsx(solo para el remitente, se oculta una vez que el mensaje ya está en modo de desaparición). Los mensajes que desaparecen a nivel de conversación (updateConversationSettings→disappearingMessagesEnabled/disappearingMessagesDuration) son la otra ruta, independiente, descrita más abajo -
shareLocation— conectado medianteChatView.tsx#handleShareLocation(geolocalización del navegador → pin estático) detrás de la acción "Compartir ubicación" del compositor; renderizado enMessageBubble.tsxcon un enlace a Maps -
updateLiveLocation/stopLiveLocation— los resolvers existen enmessage.resolver.js, pero no hay ninguna referencia en el frontend dentro de los componentes de chat; el envío de ubicación en vivo también necesita que se agreguelocationTypeal input de GraphQLLocationInput(actualmente solo tienelatitude/longitude/address/name) -
updateConversationSettings(mensajes que desaparecen) —disappearingMessages/disappearingDurationse mapean a las columnas realesdisappearingMessagesEnabled/disappearingMessagesDurationenconversation.manager.js; activarlo sin una duración explícita usa por defecto 86400s (24h) -
messageTranslations/myTranslationSetting/setMessageTranslation—message-translation.resolver.js+message-translation.manager.js; los resultados se almacenan en caché por(message_id, target_language)en la tablamessage_translation; conectado en el encabezado del chat (ChatHeader.tsx) y en el panel de detalles móvil (ConversationDetailsPanel.tsx) medianteuseMessageTranslations.ts -
@mentions—createMessageanaliza@usernamey escribe filas deMessageMention; los resolversuserMentions/messageMentions/markMentionAsReady el tipo GraphQLMessageMentionexisten y son funcionales, pero ninguno de los tres tiene un consumidor en el frontend — todavía no hay interfaz de historial de menciones dentro del chat
Tipos de mensaje
enum MessageType {
text · link · image · video · audio · file
post · voice · gif · sticker · location · poll
coin_transfer
}
Modelo Message — campos clave
| Campo | Descripción |
|---|---|
messageType | Tipo de mensaje (ver el enum anterior) |
replyToMessageId | Referencia al mensaje citado |
sharedPostId | Post compartido dentro del chat |
sharedConversationId | Conversación compartida como enlace |
sharedUserId | Perfil de usuario compartido en el chat |
isPaid / price | Mensaje de pago con un precio |
unlockPrice | Precio en monedas para desbloquear contenido bloqueado |
isDisappearing / disappearsAt | Mensajes efímeros |
viewOnce | El destinatario puede verlo solo una vez |
isPinned | Fijado en la conversación |
isEdited / editHistory | Historial completo de ediciones |
poll | Encuesta incrustada |
location / locationType | Ubicación estática o en vivo |
Consultas principales
conversationMessages devuelve un historial de mensajes paginado para una conversación, ordenado del más reciente al más antiguo. Envía before (un ID de mensaje) para la paginación basada en cursor — carga mensajes más antiguos a medida que el usuario se desplaza hacia arriba.
pinnedMessages devuelve los mensajes que el administrador de la conversación ha fijado — muéstralos en una barra colapsable de "Fijados" en la parte superior del chat.
starredMessages devuelve los mensajes que el usuario actual ha destacado en todas las conversaciones — útil para una bandeja de "Mensajes guardados".
unreadCount devuelve el conteo de mensajes no leídos para una conversación específica, o de todas las conversaciones si se omite conversationId.
searchMessages realiza una búsqueda de texto completo dentro de una conversación. La respuesta incluye hasMore para la paginación.
query ConversationMessages($conversationId: ID!, $limit: Int, $before: String) {
conversationMessages(conversationId: $conversationId, limit: $limit, before: $before) {
id messageType messageText mediaUrls isPaid unlockPrice
sender { username profilePicture }
replyTo { id messageText }
reactions { emoji user { username } }
readReceipts { user { username } readAt }
}
}
query PinnedMessages($conversationId: ID!) { pinnedMessages(conversationId: $conversationId) { id messageText } }
query StarredMessages { starredMessages { id message { messageText } } }
query UnreadCount($conversationId: ID) { unreadCount(conversationId: $conversationId) }
query SearchMessages($conversationId: ID!, $query: String!) {
searchMessages(conversationId: $conversationId, query: $query) {
messages { id messageText createdAt }
total hasMore
}
}
Mutaciones
sendMessage es la mutación principal de envío. Configura messageType para controlar qué campos multimedia son obligatorios. La mutación devuelve el objeto de mensaje completo para que el cliente pueda añadirlo a la lista de inmediato.
pinMessage destaca un mensaje en la barra de fijados; starMessage lo guarda en la lista personal de destacados del usuario (no visible para otros).
addReaction adjunta un emoji a un mensaje. Se permiten múltiples reacciones de diferentes usuarios.
forwardMessage copia el mensaje a una o más conversaciones. forwardedCount confirma cuántos se enviaron.
replyToMessage crea un nuevo mensaje con replyToMessageId establecido, mostrando el mensaje citado encima de la respuesta.
setMessageExpiration hace que un mensaje desaparezca después de expirationSeconds. Llama a esto sobre un mensaje ya existente para activar el modo de desaparición. setViewOnce hace que un mensaje sea visible solo una vez por el destinatario — después de verlo, se elimina permanentemente.
shareLocation envía un pin de ubicación estática. updateLiveLocation actualiza la posición de un mensaje de seguimiento en vivo. stopLiveLocation finaliza el compartir en vivo.
createPoll incrusta una encuesta en el chat. voteInPoll registra un voto para una opción.
scheduleMessage encola un mensaje para entrega futura en scheduledFor. cancelScheduledMessage lo elimina de la cola.
saveMessageDraft almacena un borrador no enviado en la base de datos — se recupera cuando el usuario vuelve a abrir la conversación.
# Send a message — messageType controls which media fields are required
mutation SendMessage($input: MessageCreateInput!) { sendMessage(input: $input) { id messageType messageText } }
# Conversation-level pin (visible to all participants)
mutation PinMessage($messageId: ID!) { pinMessage(messageId: $messageId) { id } }
# Personal star (private, like bookmarking a message)
mutation StarMessage($messageId: ID!) { starMessage(messageId: $messageId) { id } }
# Attach an emoji reaction to a message
mutation AddReaction($messageId: ID!, $emoji: String!) { addReaction(messageId: $messageId, emoji: $emoji) { id emoji } }
# Copy message to other conversations
mutation ForwardMessage($messageId: ID!, $conversationIds: [ID!]!) { forwardMessage(messageId: $messageId, conversationIds: $conversationIds) { forwardedCount } }
# Quote-reply (creates a new message with replyToMessageId set)
mutation ReplyTo($messageId: ID!, $input: MessageCreateInput!) { replyToMessage(messageId: $messageId, input: $input) { id } }
# Make a message self-destruct after N seconds
mutation SetExpiration($messageId: ID!, $seconds: Int!) { setMessageExpiration(messageId: $messageId, expirationSeconds: $seconds) { disappearsAt } }
# Make a message viewable only once — deleted immediately after the recipient views it
mutation SetViewOnce($messageId: ID!) { setViewOnce(messageId: $messageId) { viewOnce } }
# Share a static location pin
mutation ShareLocation($input: LocationInput!) { shareLocation(input: $input) { id location { latitude longitude } } }
# Update position for a live-location message
mutation UpdateLiveLocation($messageId: ID!, $lat: Float!, $lng: Float!) { updateLiveLocation(messageId: $messageId, latitude: $lat, longitude: $lng) { id } }
# Stop broadcasting live location
mutation StopLiveLocation($messageId: ID!) { stopLiveLocation(messageId: $messageId) { id } }
# Create a poll inside a conversation
mutation CreatePoll($conversationId: ID!, $input: PollInput!) { createPoll(conversationId: $conversationId, input: $input) { id poll { question options { text votes } } } }
# Record a vote on a poll option
mutation VoteInPoll($messageId: ID!, $optionId: ID!) { voteInPoll(messageId: $messageId, optionId: $optionId) { id } }
# Queue a message to be sent at a future time
mutation ScheduleMessage($input: MessageCreateInput!, $scheduledFor: DateTime!) { scheduleMessage(input: $input, scheduledFor: $scheduledFor) { id } }
# Remove a scheduled message before it sends
mutation CancelScheduled($messageId: ID!) { cancelScheduledMessage(messageId: $messageId) }
# Transfer coins directly via a message
mutation SendCoins($conversationId: ID!, $recipientId: ID, $amount: Int!, $message: String) {
sendCoinsViaMessage(conversationId: $conversationId, recipientId: $recipientId, amount: $amount, message: $message) {
success transaction { amount balanceAfter }
}
}
# Persist an unsent draft (retrieved on conversation open)
mutation SaveDraft($conversationId: ID!, $content: String!) { saveMessageDraft(conversationId: $conversationId, content: $content) { id } }
Suscripciones en tiempo real
Suscríbete a estas en la pantalla de chat para que la interfaz se actualice sin hacer polling.
# New message sent by anyone in the conversation
subscription MessageAdded($conversationId: ID!) { messageAdded(conversationId: $conversationId) { id messageText sender { username } } }
# Existing message was edited
subscription MessageUpdated($conversationId: ID!) { messageUpdated(conversationId: $conversationId) { id isEdited } }
# Message was deleted
subscription MessageDeleted($conversationId: ID!) { messageDeleted(conversationId: $conversationId) { messageId } }
# Shows "Alice is typing..." indicator
subscription TypingIndicator($conversationId: ID!) { typingIndicator(conversationId: $conversationId) { userId username isTyping } }
# Message was read by another participant
subscription MessageRead($conversationId: ID!) { messageRead(conversationId: $conversationId) { messageId userId readAt } }
# A reaction was added to a message in the conversation
subscription MessageReactionAdded($conversationId: ID!) { messageReactionAdded(conversationId: $conversationId) { id messageId userId emoji createdAt } }
Traducción del chat
Preferencia de traducción por conversación y por participante, almacenada en ConversationParticipant (translationEnabled / preferredLanguage) y leída/escrita por message-translation.manager.js. El texto traducido se almacena en caché por (message_id, target_language) en la tabla message_translation, de modo que un mensaje solo se envía a la API de traducción de pago una vez por idioma. myTranslationSetting también devuelve available: false (ocultando la función en el cliente) cuando no hay ningún proveedor de traducción configurado mediante variables de entorno.
type MessageTranslation { messageId: ID! text: String! sourceLanguage: String targetLanguage: String! }
type TranslationSetting { enabled: Boolean! language: String available: Boolean! }
# Cache-first translations for the given messages, in the viewer's reading language
query MessageTranslations($messageIds: [ID!]!) { messageTranslations(messageIds: $messageIds) { messageId text sourceLanguage targetLanguage } }
# The viewer's current translation preference for a conversation
query MyTranslationSetting($conversationId: ID!) { myTranslationSetting(conversationId: $conversationId) { enabled language available } }
# Turn translation on/off and set the reading language for the caller
mutation SetMessageTranslation($conversationId: ID!, $enabled: Boolean!, $language: String) {
setMessageTranslation(conversationId: $conversationId, enabled: $enabled, language: $language) { enabled language available }
}
Conversaciones
Tipos
- DM — conversación directa entre dos usuarios
- Group — múltiples participantes con roles
admin/member - Subscription — requiere un pago en monedas para unirse (
isSubscriptionRequired,subscriptionPriceCoins)
Consultas
myConversations devuelve la lista de conversaciones ordenada por última actividad — la vista principal de la bandeja de entrada. unreadCount en cada elemento alimenta la insignia. lastMessage proporciona el texto de vista previa.
archivedConversations devuelve las conversaciones que el usuario ha archivado (ocultas de la bandeja principal). lockedConversations devuelve las conversaciones que el usuario ha bloqueado con un código de acceso.
totalUnreadCount es un entero global para la insignia de mensajes a nivel de app.
query MyConversations($limit: Int, $offset: Int) {
myConversations(limit: $limit, offset: $offset) {
id type name avatarUrl unreadCount
lastMessage { messageText createdAt }
participants { user { username profilePicture } role }
}
}
query ArchivedConversations { archivedConversations { id name } }
query LockedConversations { lockedConversations { id name } }
query TotalUnread { totalUnreadCount }
Configuración de conversación
Las conversaciones grupales admiten configuraciones por conversación:
onlyAdminsCanSend restringe la publicación solo a administradores — útil para canales de anuncios. disappearingMessages habilita la eliminación automática para todos los mensajes nuevos de la conversación. disappearingDuration establece el TTL en segundos.
mutation UpdateConversationSettings($conversationId: ID!, $settings: ConversationSettingsInput!) {
updateConversationSettings(conversationId: $conversationId, settings: $settings) { id }
}
Campos de ConversationSettingsInput: onlyAdminsCanSend (restringe la publicación a administradores), allowInviteMembers, disappearingMessages (booleano), disappearingDuration (segundos).
Mutaciones de gestión
createConversation crea un DM o un grupo. addParticipant añade un usuario a un grupo existente. updateParticipantRole promueve/degrada entre admin y member. transferAdmin transfiere la propiedad de administrador a otro participante (el administrador que llama pierde su rol).
muteConversation suprime las notificaciones push durante duration minutos (o indefinidamente si se omite). archiveConversation oculta la conversación de la bandeja principal. pinConversation la mantiene en la parte superior de la lista. lockConversation / unlockConversation activan o desactivan el bloqueo con código de acceso. blockConversation impide que el otro usuario envíe nuevos mensajes. clearConversationHistory elimina todos los mensajes localmente (el servidor puede conservarlos para el otro participante). reportConversation presenta un reporte contra la conversación para revisión del equipo de administración. setTypingStatus transmite el indicador de "escribiendo".
mutation CreateConversation($input: ConversationCreateInput!) { createConversation(input: $input) { id } }
mutation AddParticipant($conversationId: ID!, $userId: ID!) { addParticipant(conversationId: $conversationId, userId: $userId) { id role } }
mutation UpdateRole($conversationId: ID!, $participantId: ID!, $role: ParticipantRole!) { updateParticipantRole(conversationId: $conversationId, participantId: $participantId, role: $role) { role } }
mutation TransferAdmin($conversationId: ID!, $newAdminId: ID!) { transferAdmin(conversationId: $conversationId, newAdminId: $newAdminId) }
mutation MuteConversation($id: ID!, $duration: Int) { muteConversation(conversationId: $id, duration: $duration) }
mutation ArchiveConversation($id: ID!) { archiveConversation(conversationId: $id) }
mutation PinConversation($id: ID!) { pinConversation(conversationId: $id) }
mutation LockConversation($id: ID!) { lockConversation(conversationId: $id) }
mutation UnlockConversation($id: ID!) { unlockConversation(conversationId: $id) }
mutation BlockConversation($id: ID!) { blockConversation(conversationId: $id) }
mutation ClearHistory($id: ID!) { clearConversationHistory(conversationId: $id) }
mutation ReportConversation($id: ID!, $input: ConversationReportInput!) { reportConversation(conversationId: $id, input: $input) { status } }
mutation SetTyping($id: ID!, $isTyping: Boolean!) { setTypingStatus(conversationId: $id, isTyping: $isTyping) }
Suscripciones
# Fires when a new conversation is created involving the user (e.g. someone DMs them)
subscription ConversationAdded($userId: ID!) { conversationAdded(userId: $userId) { id type } }
# Fires when a conversation's metadata changes (name, last message, etc.)
subscription ConversationUpdated($conversationId: ID!) { conversationUpdated(conversationId: $conversationId) { id lastMessage { messageText } } }
Mensajes de pago
Los creadores pueden bloquear mensajes individuales detrás de un precio en monedas. Solo los tipos de mensaje image o video se pueden monetizar — los mensajes de solo texto no pueden ser de pago.
lockMessage establece un unlockPrice en monedas sobre un mensaje. unlockMessage elimina el bloqueo (acceso gratuito). purchaseMessage deduce monedas de la billetera del comprador, otorga acceso y acredita al creador (menos la comisión de la plataforma). refundMessagePurchase revierte una compra.
hasMessageAccess es una verificación booleana para renderizar el estado de bloqueado/desbloqueado. myMessagePurchases lista lo que el usuario ha desbloqueado. messagePurchaseStats devuelve análisis de ingresos para un mensaje específico. myCreatorEarnings agrega todas las ganancias por mensajes de pago del creador autenticado.
# Lock a message behind a coin price
mutation LockMessage($input: LockMessageInput!) { lockMessage(input: $input) { id unlockPrice } }
# Remove the lock (make free again)
mutation UnlockMessage($messageId: ID!) { unlockMessage(messageId: $messageId) }
# Purchase access — deducts coins and grants access
mutation PurchaseMessage($input: PurchaseMessageInput!) {
purchaseMessage(input: $input) { id status amount creatorEarningsCoins platformFeeCoins purchasedAt }
}
# Issue a refund (admin or creator)
mutation RefundMessagePurchase($purchaseId: ID!, $reason: String) {
refundMessagePurchase(purchaseId: $purchaseId, reason: $reason) { id status refundedAt }
}
# Check if the caller has already unlocked this message
query HasAccess($messageId: ID!) { hasMessageAccess(messageId: $messageId) }
# Buyer's purchase history
query MyMessagePurchases($limit: Int) { myMessagePurchases(limit: $limit) { id amount status purchasedAt message { id } } }
# Revenue breakdown for a specific message
query MessagePurchaseStats($messageId: ID!) {
messagePurchaseStats(messageId: $messageId) {
totalPurchases totalRevenue totalPlatformFees totalCreatorEarnings averagePurchaseAmount
}
}
# Creator's total earnings across all paid messages
query CreatorEarnings { myCreatorEarnings { totalSales totalRevenue totalEarnings platformFees } }
Valores de PurchaseStatus: pending / completed / refunded / failed. Los mensajes de pago deben ser del tipo image o video — los mensajes de solo texto no pueden monetizarse.
Conversaciones con acceso restringido por suscripción
Una conversación grupal puede requerir un pago recurrente en monedas para unirse, independientemente del sistema de suscripción a creadores.
enableConversationSubscription añade un requisito de precio en monedas a una conversación grupal existente. disableConversationSubscription lo elimina (los suscriptores existentes conservan el acceso hasta que termine su período).
grantFreeConversationAccess otorga acceso gratuito a un usuario específico — útil para moderadores o invitados VIP.
subscribeToConversation paga el precio en monedas y otorga acceso. cancelConversationSubscription cancela una suscripción — el acceso continúa hasta currentPeriodEnd.
hasConversationAccess es una verificación booleana — úsala antes de cargar el historial de mensajes para decidir si mostrar el aviso de suscripción. mySubscriptionEarnings agrega las ganancias de por vida del creador. conversationSubscriptionStats devuelve métricas por conversación.
# Enable subscription requirement on a group conversation
mutation EnableSub($input: EnableSubscriptionInput!) {
enableConversationSubscription(input: $input) { id isSubscriptionRequired subscriptionPriceCoins }
}
# Remove subscription requirement
mutation DisableSub($conversationId: ID!) {
disableConversationSubscription(conversationId: $conversationId) { id }
}
# Give a specific user free access (e.g. moderators, VIPs)
mutation GrantFreeAccess($conversationId: ID!, $userId: ID!) {
grantFreeConversationAccess(conversationId: $conversationId, userId: $userId) { id isFree status }
}
# Pay the coin price and join the conversation
mutation Subscribe($input: SubscribeToConversationInput!) {
subscribeToConversation(input: $input) {
id coinPrice isFree status
currentPeriodStart currentPeriodEnd nextBillingDate
}
}
# Cancel — access continues until end of current period
mutation CancelSub($subscriptionId: ID!) {
cancelConversationSubscription(subscriptionId: $subscriptionId) { id status cancelledAt }
}
# Check if the caller has access before loading the chat
query HasAccess($conversationId: ID!) { hasConversationAccess(conversationId: $conversationId) }
# Creator's lifetime subscription earnings
query SubEarnings {
mySubscriptionEarnings {
totalConversations totalSubscriptions
totalRevenue totalEarnings platformFees
}
}
# Per-conversation subscription metrics
query SubStats($conversationId: ID!) {
conversationSubscriptionStats(conversationId: $conversationId) {
totalSubscriptions activeSubscriptions cancelledSubscriptions
totalRevenue averagePrice
}
}
Valores de SubscriptionStatus: active / cancelled / expired / pending.