diff --git a/sources/tech/20180720 Building a Messenger App- Conversation Page.md b/sources/tech/20180720 Building a Messenger App- Conversation Page.md new file mode 100644 index 0000000000..c721b48161 --- /dev/null +++ b/sources/tech/20180720 Building a Messenger App- Conversation Page.md @@ -0,0 +1,269 @@ +[#]: collector: (lujun9972) +[#]: translator: ( ) +[#]: reviewer: ( ) +[#]: publisher: ( ) +[#]: url: ( ) +[#]: subject: (Building a Messenger App: Conversation Page) +[#]: via: (https://nicolasparada.netlify.com/posts/go-messenger-conversation-page/) +[#]: author: (Nicolás Parada https://nicolasparada.netlify.com/) + +Building a Messenger App: Conversation Page +====== + +This post is the 9th and last in a series: + + * [Part 1: Schema][1] + * [Part 2: OAuth][2] + * [Part 3: Conversations][3] + * [Part 4: Messages][4] + * [Part 5: Realtime Messages][5] + * [Part 6: Development Login][6] + * [Part 7: Access Page][7] + * [Part 8: Home Page][8] + + + +In this post we’ll code the conversation page. This page is the chat between the two users. At the top we’ll show info about the other participant, below, a list of the latest messages and a message form at the bottom. + +### Chat heading + +![chat heading screenshot][9] + +Let’s start by creating the file `static/pages/conversation-page.js` with the following content: + +``` +import http from '../http.js' +import { navigate } from '../router.js' +import { avatar, escapeHTML } from '../shared.js' + +export default async function conversationPage(conversationID) { + let conversation + try { + conversation = await getConversation(conversationID) + } catch (err) { + alert(err.message) + navigate('/', true) + return + } + + const template = document.createElement('template') + template.innerHTML = ` +
+ ← Back + ${avatar(conversation.otherParticipant)} + ${conversation.otherParticipant.username} +
+ + + ` + const page = template.content + return page +} + +function getConversation(id) { + return http.get('/api/conversations/' + id) +} +``` + +This page receives the conversation ID the router extracted from the URL. + +First it does a GET request to `/api/conversations/{conversationID}` to get info about the conversation. In case of error, we show it and redirect back to `/`. Then we render info about the other participant. + +### Conversation List + +![chat heading screenshot][10] + +We’ll fetch the latest messages too to display them. + +``` +let conversation, messages +try { + [conversation, messages] = await Promise.all([ + getConversation(conversationID), + getMessages(conversationID), + ]) +} +``` + +Update the `conversationPage()` function to fetch the messages too. We use `Promise.all()` to do both request at the same time. + +``` +function getMessages(conversationID) { + return http.get(`/api/conversations/${conversationID}/messages`) +} +``` + +A GET request to `/api/conversations/{conversationID}/messages` gets the latest messages of the conversation. + +``` +
    +``` + +Now, add that list to the markup. + +``` +const messagesOList = page.getElementById('messages') +for (const message of messages.reverse()) { + messagesOList.appendChild(renderMessage(message)) +} +``` + +So we can append messages to the list. We show them in reverse order. + +``` +function renderMessage(message) { + const messageContent = escapeHTML(message.content) + const messageDate = new Date(message.createdAt).toLocaleString() + + const li = document.createElement('li') + if (message.mine) { + li.classList.add('owned') + } + li.innerHTML = ` +

    ${messageContent}

    + + ` + return li +} +``` + +Each message item displays the message content itself with its timestamp. Using `.mine` we can append a different class to the item so maybe you can show the message to the right. + +### Message Form + +![chat heading screenshot][11] + +``` +
    + + +
    +``` + +Add that form to the current markup. + +``` +page.getElementById('message-form').onsubmit = messageSubmitter(conversationID) +``` + +Attach an event listener to the “submit” event. + +``` +function messageSubmitter(conversationID) { + return async ev => { + ev.preventDefault() + + const form = ev.currentTarget + const input = form.querySelector('input') + const submitButton = form.querySelector('button') + + input.disabled = true + submitButton.disabled = true + + try { + const message = await createMessage(input.value, conversationID) + input.value = '' + const messagesOList = document.getElementById('messages') + if (messagesOList === null) { + return + } + + messagesOList.appendChild(renderMessage(message)) + } catch (err) { + if (err.statusCode === 422) { + input.setCustomValidity(err.body.errors.content) + } else { + alert(err.message) + } + } finally { + input.disabled = false + submitButton.disabled = false + + setTimeout(() => { + input.focus() + }, 0) + } + } +} + +function createMessage(content, conversationID) { + return http.post(`/api/conversations/${conversationID}/messages`, { content }) +} +``` + +We make use of [partial application][12] to have the conversation ID in the “submit” event handler. It takes the message content from the input and does a POST request to `/api/conversations/{conversationID}/messages` with it. Then prepends the newly created message to the list. + +### Messages Subscription + +To make it realtime we’ll subscribe to the message stream in this page also. + +``` +page.addEventListener('disconnect', subscribeToMessages(messageArriver(conversationID))) +``` + +Add that line in the `conversationPage()` function. + +``` +function subscribeToMessages(cb) { + return http.subscribe('/api/messages', cb) +} + +function messageArriver(conversationID) { + return message => { + if (message.conversationID !== conversationID) { + return + } + + const messagesOList = document.getElementById('messages') + if (messagesOList === null) { + return + + } + messagesOList.appendChild(renderMessage(message)) + readMessages(message.conversationID) + } +} + +function readMessages(conversationID) { + return http.post(`/api/conversations/${conversationID}/read_messages`) +} +``` + +We also make use of partial application to have the conversation ID here. +When a new message arrives, first we check if it’s from this conversation. If it is, we go a prepend a message item to the list and do a POST request to `/api/conversations/{conversationID}/read_messages` to updated the last time the participant read messages. + +* * * + +That concludes this series. The messenger app is now functional. + +~~I’ll add pagination on the conversation and message list, also user searching before sharing the source code. I’ll updated once it’s ready along with a hosted demo 👨‍💻~~ + +[Souce Code][13] • [Demo][14] + +-------------------------------------------------------------------------------- + +via: https://nicolasparada.netlify.com/posts/go-messenger-conversation-page/ + +作者:[Nicolás Parada][a] +选题:[lujun9972][b] +译者:[译者ID](https://github.com/译者ID) +校对:[校对者ID](https://github.com/校对者ID) + +本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 + +[a]: https://nicolasparada.netlify.com/ +[b]: https://github.com/lujun9972 +[1]: https://nicolasparada.netlify.com/posts/go-messenger-schema/ +[2]: https://nicolasparada.netlify.com/posts/go-messenger-oauth/ +[3]: https://nicolasparada.netlify.com/posts/go-messenger-conversations/ +[4]: https://nicolasparada.netlify.com/posts/go-messenger-messages/ +[5]: https://nicolasparada.netlify.com/posts/go-messenger-realtime-messages/ +[6]: https://nicolasparada.netlify.com/posts/go-messenger-dev-login/ +[7]: https://nicolasparada.netlify.com/posts/go-messenger-access-page/ +[8]: https://nicolasparada.netlify.com/posts/go-messenger-home-page/ +[9]: https://nicolasparada.netlify.com/img/go-messenger-conversation-page/heading.png +[10]: https://nicolasparada.netlify.com/img/go-messenger-conversation-page/list.png +[11]: https://nicolasparada.netlify.com/img/go-messenger-conversation-page/form.png +[12]: https://en.wikipedia.org/wiki/Partial_application +[13]: https://github.com/nicolasparada/go-messenger-demo +[14]: https://go-messenger-demo.herokuapp.com/