Permalink
Browse files

Create and retrieve messages through RoomChannel

  • Loading branch information...
bnhansn committed Oct 22, 2016
1 parent 1c1a394 commit 9b698795c0c91f244a80cb7315230cda66a44de6
@@ -1,3 +1,4 @@
defmodule Sling.Repo do
use Ecto.Repo, otp_app: :sling
use Scrivener, page_size: 25
end
@@ -19,7 +19,7 @@ defmodule Sling.Mixfile do
def application do
[mod: {Sling, []},
applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,
:phoenix_ecto, :postgrex, :comeonin]]
:phoenix_ecto, :postgrex, :comeonin, :scrivener_ecto]]
end
# Specifies which paths to compile per environment.
@@ -40,7 +40,8 @@ defmodule Sling.Mixfile do
{:cowboy, "~> 1.0"},
{:comeonin, "~> 2.5"},
{:guardian, "~> 0.13.0"},
{:cors_plug, "~> 1.1"}]
{:cors_plug, "~> 1.1"},
{:scrivener_ecto, "~> 1.0"}]
end
# Aliases are shortcuts or tasks specific to the current project.
@@ -22,4 +22,6 @@
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []},
"postgrex": {:hex, :postgrex, "0.12.1", "2f8b46cb3a44dcd42f42938abedbfffe7e103ba4ce810ccbeee8dcf27ca0fb06", [:mix], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.0-rc.4", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]},
"ranch": {:hex, :ranch, "1.2.1", "a6fb992c10f2187b46ffd17ce398ddf8a54f691b81768f9ef5f461ea7e28c762", [:make], []},
"scrivener": {:hex, :scrivener, "2.1.1", "eb52c8b7d283e8999edd6fd50d872ab870669d1f4504134841d0845af11b5ef3", [:mix], []},
"scrivener_ecto": {:hex, :scrivener_ecto, "1.0.2", "4b10a2e6c23ed8aae59731d7ae71bfd55afea6559aae61b124e6e521055b4a9c", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:postgrex, "~> 0.11.0 or ~> 0.12.0", [hex: :postgrex, optional: true]}, {:scrivener, "~> 2.0", [hex: :scrivener, optional: false]}]},
"uuid": {:hex, :uuid, "1.1.5", "96cb36d86ee82f912efea4d50464a5df606bf3f1163d6bdbb302d98474969369", [:mix], []}}
@@ -4,14 +4,44 @@ defmodule Sling.RoomChannel do
def join("rooms:" <> room_id, _params, socket) do
room = Repo.get!(Sling.Room, room_id)
page =
Sling.Message
|> where([m], m.room_id == ^room.id)
|> order_by([desc: :inserted_at, desc: :id])
|> preload(:user)
|> Sling.Repo.paginate()
response = %{
room: Phoenix.View.render_one(room, Sling.RoomView, "room.json"),
messages: Phoenix.View.render_many(page.entries, Sling.MessageView, "message.json"),
pagination: Sling.PaginationHelpers.pagination(page)
}
{:ok, response, assign(socket, :room, room)}
end
def handle_in("new_message", params, socket) do
changeset =
socket.assigns.room
|> build_assoc(:messages, user_id: socket.assigns.current_user.id)
|> Sling.Message.changeset(params)
case Repo.insert(changeset) do
{:ok, message} ->
broadcast_message(socket, message)
{:reply, :ok, socket}
{:error, changeset} ->
{:reply, {:error, Phoenix.View.render(Sling.ChangesetView, "error.json", changeset: changeset)}, socket}
end
end
def terminate(_reason, socket) do
{:ok, socket}
end
defp broadcast_message(socket, message) do
message = Repo.preload(message, :user)
rendered_message = Phoenix.View.render_one(message, Sling.MessageView, "message.json")
broadcast!(socket, "message_created", rendered_message)
end
end
@@ -0,0 +1,15 @@
defmodule Sling.MessageView do
use Sling.Web, :view
def render("message.json", %{message: message}) do
%{
id: message.id,
inserted_at: message.inserted_at,
text: message.text,
user: %{
email: message.user.email,
username: message.user.username
}
}
end
end
@@ -0,0 +1,10 @@
defmodule Sling.PaginationHelpers do
def pagination(page) do
%{
page_number: page.page_number,
page_size: page.page_size,
total_pages: page.total_pages,
total_entries: page.total_entries
}
end
end
@@ -1,8 +1,14 @@
import { reset } from 'redux-form';
export function connectToChannel(socket, roomId) {
return (dispatch) => {
if (!socket) { return false; }
const channel = socket.channel(`rooms:${roomId}`);
channel.on('message_created', (message) => {
dispatch({ type: 'MESSAGE_CREATED', message });
});
channel.join().receive('ok', (response) => {
dispatch({ type: 'ROOM_CONNECTED_TO_CHANNEL', response, channel });
});
@@ -19,3 +25,13 @@ export function leaveChannel(channel) {
dispatch({ type: 'USER_LEFT_ROOM' });
};
}
export function createMessage(channel, data) {
return dispatch => new Promise((resolve, reject) => {
channel.push('new_message', data)
.receive('ok', () => resolve(
dispatch(reset('newMessage'))
))
.receive('error', () => reject());
});
}
@@ -10,7 +10,6 @@ function connectToSocket(dispatch) {
const token = JSON.parse(localStorage.getItem('token'));
const socket = new Socket(`${WEBSOCKET_URL}/socket`, {
params: { token },
logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data); }
});
socket.connect();
dispatch({ type: 'SOCKET_CONNECTED', socket });
@@ -0,0 +1,24 @@
// @flow
import React from 'react';
import md5 from 'md5';
type Props = {
email: string,
size?: number,
style?: Object,
}
const Avatar = ({ email, size = 40, style }: Props) => {
const hash = md5(email);
const uri = `https://secure.gravatar.com/avatar/${hash}`;
return (
<img
src={uri}
alt={email}
style={{ width: `${size}px`, height: `${size}px`, borderRadius: '4px', ...style }}
/>
);
};
export default Avatar;
@@ -0,0 +1,29 @@
// @flow
import React from 'react';
import moment from 'moment';
import Avatar from '../Avatar';
type Props = {
message: {
text: string,
inserted_at: string,
user: {
email: string,
username: string,
},
}
}
const Message = ({ message: { text, inserted_at, user } }: Props) =>
<div style={{ display: 'flex', marginBottom: '10px' }}>
<Avatar email={user.email} style={{ marginRight: '10px' }} />
<div>
<div style={{ lineHeight: '1.2' }}>
<b style={{ marginRight: '8px', fontSize: '14px' }}>{user.username}</b>
<time style={{ fontSize: '12px', color: 'rgb(192,192,192)' }}>{moment(inserted_at).format('h:mm A')}</time>
</div>
<div>{text}</div>
</div>
</div>;
export default Message;
@@ -0,0 +1,73 @@
// @flow
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import { css, StyleSheet } from 'aphrodite';
const styles = StyleSheet.create({
form: {
padding: '0px 10px 10px 10px',
background: '#fff',
},
input: {
borderWidth: '2px',
borderColor: 'rgb(214,214,214)',
},
button: {
color: 'rgb(80,80,80)',
background: 'rgb(214,214,214)',
borderWidth: '2px',
borderColor: 'rgb(214,214,214)',
},
});
type Props = {
onSubmit: () => void,
handleSubmit: () => void,
submitting: boolean,
}
class MessageForm extends Component {
props: Props
handleSubmit = data => this.props.onSubmit(data);
render() {
const { handleSubmit, submitting } = this.props;
return (
<form onSubmit={handleSubmit(this.handleSubmit)} className={css(styles.form)}>
<div className="input-group">
<Field
name="text"
type="text"
component="input"
className={`form-control ${css(styles.input)}`}
/>
<div className="input-group-btn">
<button
disabled={submitting}
className={`btn ${css(styles.button)}`}
>
Send
</button>
</div>
</div>
</form>
);
}
}
const validate = (values) => {
const errors = {};
if (!values.text) {
errors.text = 'Required';
}
return errors;
};
export default reduxForm({
form: 'newMessage',
validate,
})(MessageForm);
@@ -0,0 +1,88 @@
// @flow
import React, { Component } from 'react';
import moment from 'moment';
import groupBy from 'lodash/groupBy';
import mapKeys from 'lodash/mapKeys';
import { css, StyleSheet } from 'aphrodite';
import Message from '../Message';
const styles = StyleSheet.create({
container: {
flex: '1',
padding: '10px 10px 0 10px',
background: '#fff',
overflowY: 'auto',
},
dayDivider: {
position: 'relative',
margin: '1rem 0',
textAlign: 'center',
'::after': {
position: 'absolute',
top: '50%',
right: '0',
left: '0',
height: '1px',
background: 'rgb(240,240,240)',
content: '""',
},
},
dayText: {
zIndex: '1',
position: 'relative',
background: '#fff',
padding: '0 12px',
},
});
type MessageType = {
id: number,
inserted_at: string,
}
type Props = {
messages: Array<MessageType>,
}
class MessageList extends Component {
props: Props
renderMessages = messages =>
messages.map(message => <Message key={message.id} message={message} />);
renderDays() {
const { messages } = this.props;
messages.map(message => message.day = moment(message.inserted_at).format('MMMM Do')); // eslint-disable-line
const dayGroups = groupBy(messages, 'day');
const days = [];
mapKeys(dayGroups, (value, key) => {
days.push({ date: key, messages: value });
});
const today = moment().format('MMMM Do');
const yesterday = moment().subtract(1, 'days').format('MMMM Do');
return days.map(day =>
<div key={day.date}>
<div className={css(styles.dayDivider)}>
<span className={css(styles.dayText)}>
{day.date === today && 'Today'}
{day.date === yesterday && 'Yesterday'}
{![today, yesterday].includes(day.date) && day.date}
</span>
</div>
{this.renderMessages(day.messages)}
</div>
);
}
render() {
return (
<div className={css(styles.container)}>
{this.renderDays()}
</div>
);
}
}
export default MessageList;
@@ -0,0 +1,24 @@
// @flow
import React from 'react';
import { css, StyleSheet } from 'aphrodite';
const styles = StyleSheet.create({
navbar: {
padding: '15px',
background: '#fff',
borderBottom: '1px solid rgb(240,240,240)',
},
});
type Props = {
room: {
name: string,
},
}
const RoomNavbar = ({ room }: Props) =>
<nav className={css(styles.navbar)}>
<div>#{room.name}</div>
</nav>;
export default RoomNavbar;
Oops, something went wrong.

0 comments on commit 9b69879

Please sign in to comment.