diff --git a/demo/api/conversations/controllers/conversationsController.ts b/demo/api/conversations/controllers/conversationsController.ts new file mode 100644 index 00000000..b876ec17 --- /dev/null +++ b/demo/api/conversations/controllers/conversationsController.ts @@ -0,0 +1,12 @@ +import { Request, Response } from 'express'; +import { createConversation, getConversation } from '../inMemoryDb'; + +export const handleCreateConversation = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + res.json(createConversation(conversationId)); +}; + +export const handleGetConversation = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + res.json(getConversation(conversationId)); +}; diff --git a/demo/api/conversations/controllers/messagesController.ts b/demo/api/conversations/controllers/messagesController.ts new file mode 100644 index 00000000..c063280a --- /dev/null +++ b/demo/api/conversations/controllers/messagesController.ts @@ -0,0 +1,62 @@ +import * as Ably from 'ably/promises'; +import { Request, Response } from 'express'; +import { createMessage, deleteMessage, editMessage, findMessages } from '../inMemoryDb'; + +export const handleCreateMessage = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + const ablyToken = req.headers.authorization.split(' ')[1]; + + const message = createMessage({ + ...JSON.parse(req.body), + client_id: req.headers['ably-clientid'] as string, + conversation_id: conversationId, + }); + + const client = new Ably.Rest(ablyToken); + + client.channels.get(`conversations:${conversationId}`).publish('message.created', message); + + res.json({ id: message.id }); + + res.status(201).end(); +}; + +export const handleQueryMessages = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + res.json(findMessages(conversationId, req.headers['ably-clientid'] as string)); +}; + +export const handleEditMessages = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + const ablyToken = req.headers.authorization.split(' ')[1]; + + const message = editMessage({ + id: req.params.messageId, + conversation_id: conversationId, + ...JSON.parse(req.body), + }); + + const client = new Ably.Rest(ablyToken); + + client.channels.get(`conversations:${conversationId}`).publish('message.updated', message); + + res.json({ id: message.id }); + + res.status(201).end(); +}; + +export const handleDeleteMessages = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + const ablyToken = req.headers.authorization.split(' ')[1]; + + const message = deleteMessage({ + id: req.params.messageId, + conversation_id: conversationId, + }); + + const client = new Ably.Rest(ablyToken); + + client.channels.get(`conversations:${conversationId}`).publish('message.deleted', message); + + res.status(201).end(); +}; diff --git a/demo/api/conversations/controllers/reactionsController.ts b/demo/api/conversations/controllers/reactionsController.ts new file mode 100644 index 00000000..f2ca6b8c --- /dev/null +++ b/demo/api/conversations/controllers/reactionsController.ts @@ -0,0 +1,34 @@ +import { Request, Response } from 'express'; +import * as Ably from 'ably/promises'; +import { addReaction, deleteReaction } from '../inMemoryDb'; + +export const handleAddReaction = (req: Request, res: Response) => { + const conversationId = req.params.conversationId; + const ablyToken = req.headers.authorization.split(' ')[1]; + + const reaction = addReaction({ + message_id: req.params.messageId, + conversation_id: conversationId, + client_id: req.headers['ably-clientid'] as string, + ...JSON.parse(req.body), + }); + + const client = new Ably.Rest(ablyToken); + + client.channels.get(`conversations:${conversationId}`).publish('reaction.added', reaction); + + res.status(201).end(); +}; + +export const handleDeleteReaction = (req: Request, res: Response) => { + const reactionId = req.params.reactionId; + const ablyToken = req.headers.authorization.split(' ')[1]; + + const reaction = deleteReaction(reactionId); + + const client = new Ably.Rest(ablyToken); + + client.channels.get(`conversations:${reaction.conversation_id}`).publish('reaction.deleted', reaction); + + res.status(201).end(); +}; diff --git a/demo/api/conversations/inMemoryDb.ts b/demo/api/conversations/inMemoryDb.ts new file mode 100644 index 00000000..b1295606 --- /dev/null +++ b/demo/api/conversations/inMemoryDb.ts @@ -0,0 +1,139 @@ +import { ulid } from 'ulidx'; +import exp = require('constants'); + +export interface Conversation { + id: string; + application_id: string; + ttl: number | null; + created_at: number; +} + +export interface Message { + id: string; + client_id: string; + conversation_id: string; + content: string; + reactions: { + counts: Record; + latest: Reaction[]; + mine: Reaction[]; + }; + created_at: number; + updated_at: number | null; + deleted_at: number | null; +} + +export interface Reaction { + id: string; + message_id: string; + conversation_id: string; + type: string; + client_id: string; + updated_at: number | null; + deleted_at: number | null; +} + +const conversations: Conversation[] = []; +const conversationIdToMessages: Record = {}; +const reactions: Reaction[] = []; + +export const createConversation = (id: string): Conversation => { + const existing = conversations.find((conv) => conv.id === id); + if (existing) return existing; + const conversation = { + id, + application_id: 'demo', + ttl: null, + created_at: Date.now(), + }; + conversationIdToMessages[id] = []; + conversations.push(conversation); + return conversation; +}; + +createConversation('conversation1'); + +export const getConversation = (id: string): Conversation => { + return conversations.find((conv) => conv.id === id); +}; + +export const findMessages = (conversationId: string, clientId: string) => + enrichMessagesWithReactions(conversationIdToMessages[conversationId], clientId); + +export const createMessage = (message: Pick) => { + const created: Message = { + ...message, + id: ulid(), + reactions: { + counts: {}, + latest: [], + mine: [], + }, + created_at: Date.now(), + updated_at: null, + deleted_at: null, + }; + conversationIdToMessages[created.conversation_id].push(created); + return created; +}; + +export const editMessage = (message: Pick) => { + const edited = conversationIdToMessages[message.conversation_id].find(({ id }) => message.id === id); + edited.content = message.content; + return edited; +}; + +export const deleteMessage = (message: Pick) => { + const deletedIndex = conversationIdToMessages[message.conversation_id].findIndex(({ id }) => message.id === id); + const deleted = conversationIdToMessages[message.conversation_id][deletedIndex]; + conversationIdToMessages[message.conversation_id].splice(deletedIndex, 1); + return deleted; +}; + +export const addReaction = ( + reaction: Pick, +) => { + const created: Reaction = { + ...reaction, + id: ulid(), + updated_at: null, + deleted_at: null, + }; + reactions.push(created); + return created; +}; + +export const deleteReaction = (reactionId: string) => { + const deletedIndex = reactions.findIndex((reaction) => reaction.id === reactionId); + const deleted = reactions[deletedIndex]; + reactions.splice(deletedIndex, 1); + return deleted; +}; + +const enrichMessageWithReactions = (message: Message, clientId: string): Message => { + const messageReactions = reactions.filter((reaction) => reaction.message_id === message.id); + const mine = messageReactions.filter((reaction) => reaction.client_id === clientId); + const counts = messageReactions.reduce( + (acc, reaction) => { + if (acc[reaction.type]) { + acc[reaction.type]++; + } else { + acc[reaction.type] = 1; + } + return acc; + }, + {} as Record, + ); + return { + ...message, + reactions: { + counts, + latest: messageReactions, + mine, + }, + }; +}; + +const enrichMessagesWithReactions = (messages: Message[], clientId: string) => { + return messages.map((message) => enrichMessageWithReactions(message, clientId)); +}; diff --git a/demo/api/conversations/index.ts b/demo/api/conversations/index.ts index f0da53a2..4aa3287f 100644 --- a/demo/api/conversations/index.ts +++ b/demo/api/conversations/index.ts @@ -1,50 +1,12 @@ import * as dotenv from 'dotenv'; -import * as Ably from 'ably/promises'; -import { ulid } from 'ulidx'; -import express, { Router } from 'express'; +import express from 'express'; import serverless from 'serverless-http'; +import { router } from './routes'; dotenv.config(); -const messages = []; - const api = express(); -const router = Router(); -router.post('/conversations/:conversationId/messages', (req, res) => { - const conversationId = req.params.conversationId; - const ablyToken = req.headers.authorization.split(' ')[1]; - - const message = { - id: ulid(), - ...JSON.parse(req.body), - client_id: req.headers['ably-clientid'], - conversation_id: conversationId, - reactions: { - counts: {}, - latest: [], - mine: [], - }, - created_at: Date.now(), - updated_at: null, - deleted_at: null, - }; - - messages.push(message); - - const client = new Ably.Rest(ablyToken); - - client.channels.get(`conversations:${conversationId}`).publish('message.created', message); - - res.json({ id: message.id }); - - res.status(201).end(); -}); - -router.get('/conversations/:conversationId/messages', (req, res) => { - res.json(messages); -}); - api.use('/api/conversations/v1', router); export const handler = serverless(api); diff --git a/demo/api/conversations/routes.ts b/demo/api/conversations/routes.ts new file mode 100644 index 00000000..44ca80fe --- /dev/null +++ b/demo/api/conversations/routes.ts @@ -0,0 +1,35 @@ +import { Router } from 'express'; +import { + handleCreateMessage, + handleDeleteMessages, + handleEditMessages, + handleQueryMessages, +} from './controllers/messagesController'; +import { handleCreateConversation, handleGetConversation } from './controllers/conversationsController'; +import { handleAddReaction, handleDeleteReaction } from './controllers/reactionsController'; + +const router = Router(); + +// Conversations + +router.post('/conversations/:conversationId', handleCreateConversation); + +router.get('/conversations/:conversationId', handleGetConversation); + +// Messages + +router.post('/conversations/:conversationId/messages', handleCreateMessage); + +router.get('/conversations/:conversationId/messages', handleQueryMessages); + +router.post('/conversations/:conversationId/messages/:messageId', handleEditMessages); + +router.delete('/conversations/:conversationId/messages/:messageId', handleDeleteMessages); + +// Reactions + +router.post('/conversations/:conversationId/messages/:messageId/reactions', handleAddReaction); + +router.delete('/reactions/:reactionId', handleDeleteReaction); + +export { router }; diff --git a/demo/src/containers/ConversationContext/ConversationProvider.tsx b/demo/src/containers/ConversationContext/ConversationProvider.tsx index 773beccb..55903314 100644 --- a/demo/src/containers/ConversationContext/ConversationProvider.tsx +++ b/demo/src/containers/ConversationContext/ConversationProvider.tsx @@ -15,5 +15,6 @@ export const ConversationProvider: FC = ({ client, co }), [client, conversationId], ); + return {children}; };