Create Chat app (#175)
* Setup Pusher gem * Create ChatChannel and Message model * Create ChatChannelController * Add back fix-db-schema-conflicts gem * Add pusher-js package * Add validations on Message & ChatChannel models * Update Route * Create MessageController * Update ChatChannelController * Create Chat app WIP * Extract messenger render to Message Component * Implement live chat WIP * Implement live chat WIP (2) * Add style and scrollToBottom feature for chat * Create getUserDataAndCsrfToken function * Add feature toggle to the new live chat * Clean up ChatChannelsController & create spec * Update ChatChannel spec * Update Message spec * Create MessagesController spec & refactor * Clean up Chat app * Fix lint * Add character limit to Message
This commit is contained in:
parent
943c3b392f
commit
8003191a4e
33 changed files with 623 additions and 29 deletions
2
Gemfile
2
Gemfile
|
|
@ -58,6 +58,7 @@ gem "pry", "~> 0.11"
|
|||
gem "pry-rails", "~> 0.3"
|
||||
gem "puma", "~> 3.11"
|
||||
gem "puma_worker_killer", "~> 0.1"
|
||||
gem "pusher", "~> 1.3"
|
||||
gem "pundit", "~> 1.1"
|
||||
gem "rack-host-redirect", "~> 1.3"
|
||||
gem "rack-timeout", "~> 0.4"
|
||||
|
|
@ -106,6 +107,7 @@ group :development, :test do
|
|||
gem "capybara", "~> 2.18"
|
||||
gem "derailed", "~> 0.1"
|
||||
gem "faker", "~> 1.8"
|
||||
gem "fix-db-schema-conflicts", "~> 3.0"
|
||||
gem "memory_profiler", "~> 0.9"
|
||||
gem "parallel_tests", "~> 2.21"
|
||||
gem "rspec-rails", "~> 3.7"
|
||||
|
|
|
|||
|
|
@ -273,6 +273,8 @@ GEM
|
|||
thor (~> 0.14)
|
||||
fission (0.5.0)
|
||||
CFPropertyList (~> 2.2)
|
||||
fix-db-schema-conflicts (3.0.2)
|
||||
rubocop (>= 0.38.0)
|
||||
flipflop (2.3.1)
|
||||
activesupport (>= 4.0)
|
||||
fog (1.41.0)
|
||||
|
|
@ -604,6 +606,11 @@ GEM
|
|||
puma (>= 2.7, < 4)
|
||||
pundit (1.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
pusher (1.3.1)
|
||||
httpclient (~> 2.7)
|
||||
multi_json (~> 1.0)
|
||||
pusher-signature (~> 0.1.8)
|
||||
pusher-signature (0.1.8)
|
||||
rack (2.0.4)
|
||||
rack-host-redirect (1.3.0)
|
||||
rack
|
||||
|
|
@ -917,6 +924,7 @@ DEPENDENCIES
|
|||
feedjira (~> 2.1)
|
||||
fetch-rails (~> 1.0)
|
||||
figaro (~> 1.1)
|
||||
fix-db-schema-conflicts (~> 3.0)
|
||||
flipflop (~> 2.3)
|
||||
fog (~> 1.41)
|
||||
front_matter_parser (~> 0.1)
|
||||
|
|
@ -947,6 +955,7 @@ DEPENDENCIES
|
|||
puma (~> 3.11)
|
||||
puma_worker_killer (~> 0.1)
|
||||
pundit (~> 1.1)
|
||||
pusher (~> 1.3)
|
||||
rack-host-redirect (~> 1.3)
|
||||
rack-timeout (~> 0.4)
|
||||
rack_session_access (~> 0.1)
|
||||
|
|
|
|||
70
app/assets/stylesheets/chat.scss
Normal file
70
app/assets/stylesheets/chat.scss
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// High level class
|
||||
.chatchannel {
|
||||
height: inherit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
border: 1px solid #c9c9c9;
|
||||
}
|
||||
|
||||
.chatchannel__messages {
|
||||
font-size: 15px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
height: 300px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.chatchannel__form {
|
||||
border-top: 1px solid #c9c9c9;
|
||||
background: #ededed;
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
// Chatmessage
|
||||
.chatmessage {
|
||||
display: inline-block;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.chatmessage__username {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chatmessage__divider {
|
||||
|
||||
}
|
||||
|
||||
.chatmessage__message {
|
||||
|
||||
}
|
||||
|
||||
// Messagecomposer
|
||||
.messagecomposer {
|
||||
display: flex;
|
||||
height: inherit;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.messagecomposer__input {
|
||||
border-radius: 3px;
|
||||
border: 1px solid #c9c9c9;
|
||||
font-size: 13px;
|
||||
margin: 10px;
|
||||
padding: 6px;
|
||||
resize: none;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.messagecomposer__submit {
|
||||
margin: 10px 10px 10px 0;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
background: #66e2d5;
|
||||
font-weight: 500px;
|
||||
}
|
||||
|
|
@ -27,4 +27,5 @@
|
|||
|
||||
@import 'ltags/LiquidTags';
|
||||
|
||||
@import 'chat';
|
||||
@import 'livechat/chat';
|
||||
|
|
|
|||
11
app/controllers/chat_channels_controller.rb
Normal file
11
app/controllers/chat_channels_controller.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class ChatChannelsController < ApplicationController
|
||||
def show
|
||||
@chat_channel = ChatChannel.includes(:messages).find_by(id: params[:id])
|
||||
if @chat_channel
|
||||
@chat_channel
|
||||
else
|
||||
render json: ["The chat channel you are looking for is either invalid or does not exist"],
|
||||
status: 401
|
||||
end
|
||||
end
|
||||
end
|
||||
42
app/controllers/messages_controller.rb
Normal file
42
app/controllers/messages_controller.rb
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
class MessagesController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def create
|
||||
@message = Message.new(message_params)
|
||||
success = false
|
||||
|
||||
if @message.save
|
||||
begin
|
||||
message_json = create_pusher_payload(@message)
|
||||
Pusher.trigger(@message.chat_channel.id, "message-created", message_json)
|
||||
success = true
|
||||
rescue Pusher::Error => e
|
||||
logger.info "PUSHER ERROR: #{e.message}"
|
||||
end
|
||||
|
||||
if success
|
||||
render json: ["Message created"], status: 201
|
||||
else
|
||||
result = "Message created but could not trigger Pusher"
|
||||
render json: [result, @message.to_json], status: 201
|
||||
end
|
||||
else
|
||||
render json: e.message, status: 401
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_pusher_payload(new_message)
|
||||
{
|
||||
username: new_message.user.username,
|
||||
message: new_message.message_markdown,
|
||||
timestamp: new_message.timestamp,
|
||||
color: new_message.user.bg_color_hex,
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def message_params
|
||||
params.require(:message).permit(:message_html, :user_id, :chat_channel_id)
|
||||
end
|
||||
end
|
||||
27
app/javascript/chat/actions.js
Normal file
27
app/javascript/chat/actions.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
export function getAllMessages(successCb, failureCb) {
|
||||
fetch('/chat_channels/1', {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}).then(response => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function sendMessage(message, successCb, failureCb) {
|
||||
fetch('/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message_html: message,
|
||||
user_id: window.currentUser.id,
|
||||
chat_channel_id: '1',
|
||||
}),
|
||||
credentials: 'same-origin',
|
||||
}).then(response => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
97
app/javascript/chat/chat.jsx
Normal file
97
app/javascript/chat/chat.jsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getAllMessages, sendMessage } from './actions';
|
||||
import { scrollToBottom } from './util';
|
||||
import Compose from './compose';
|
||||
import Message from './message';
|
||||
import setupPusher from './pusher';
|
||||
|
||||
class Chat extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.receiveAllMessages = this.receiveAllMessages.bind(this);
|
||||
this.receiveNewMessage = this.receiveNewMessage.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleMessageSubmit = this.handleMessageSubmit.bind(this);
|
||||
this.handleSubmitOnClick = this.handleSubmitOnClick.bind(this);
|
||||
this.handleFailure = this.handleFailure.bind(this);
|
||||
this.state = {
|
||||
messages: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
getAllMessages(this.receiveAllMessages);
|
||||
setupPusher(this.props.pusherKey, this.receiveNewMessage);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
receiveAllMessages(res) {
|
||||
this.setState({ messages: res.messages });
|
||||
}
|
||||
|
||||
receiveNewMessage(message) {
|
||||
const newMessages = this.state.messages.slice();
|
||||
newMessages.push(message);
|
||||
this.setState({ messages: newMessages });
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
this.handleMessageSubmit(e.target.value);
|
||||
e.target.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
handleMessageSubmit(message) {
|
||||
sendMessage(message, null, this.handleFailure);
|
||||
}
|
||||
|
||||
handleSubmitOnClick(e) {
|
||||
e.preventDefault();
|
||||
const message = document.getElementById('messageform').value;
|
||||
sendMessage(message, null, this.handleFailure);
|
||||
document.getElementById('messageform').value = '';
|
||||
}
|
||||
|
||||
handleFailure(err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
return this.state.messages.map(message => (
|
||||
<Message
|
||||
user={message.username}
|
||||
message={message.message}
|
||||
timeStamp={message.timestamp}
|
||||
color={message.color}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="chatchannel">
|
||||
<div className="chatchannel__messages" id="messagelist">
|
||||
{ this.renderMessage() }
|
||||
</div>
|
||||
<div className="chatchannel__form">
|
||||
<Compose
|
||||
handleKeyDown={this.handleKeyDown}
|
||||
handleSubmitOnClick={this.handleSubmitOnClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Chat.propTypes = {
|
||||
pusherKey: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
28
app/javascript/chat/compose.jsx
Normal file
28
app/javascript/chat/compose.jsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Compose = ({
|
||||
handleSubmitOnClick, handleKeyDown,
|
||||
}) => (
|
||||
<div className="messagecomposer">
|
||||
<textarea
|
||||
className="messagecomposer__input"
|
||||
id="messageform"
|
||||
placeholder="Message goes here"
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<button
|
||||
className="messagecomposer__submit"
|
||||
onClick={handleSubmitOnClick}
|
||||
>
|
||||
SEND
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
Compose.propTypes = {
|
||||
handleKeyDown: PropTypes.func.isRequired,
|
||||
handleSubmitOnClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Compose;
|
||||
27
app/javascript/chat/message.jsx
Normal file
27
app/javascript/chat/message.jsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/*
|
||||
* The prop also contain timeStamp, which is currently not in used
|
||||
*/
|
||||
|
||||
const Message = ({
|
||||
user, message, color,
|
||||
}) => {
|
||||
const spanStyle = { color };
|
||||
return (
|
||||
<div className="chatmessage">
|
||||
<span className="chatmessage__username" style={spanStyle}>{ user }</span>
|
||||
<span className="chatmessage__divider">: </span>
|
||||
<span className="chatmessage__message">{ message } </span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Message.propTypes = {
|
||||
user: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Message;
|
||||
11
app/javascript/chat/pusher.js
Normal file
11
app/javascript/chat/pusher.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import Pusher from 'pusher-js';
|
||||
|
||||
export default function setupPusher(key, callback) {
|
||||
const pusher = new Pusher(key, {
|
||||
cluster: 'us2',
|
||||
encrypted: true,
|
||||
});
|
||||
|
||||
const channel = pusher.subscribe('1');
|
||||
channel.bind('message-created', callback);
|
||||
}
|
||||
32
app/javascript/chat/util.js
Normal file
32
app/javascript/chat/util.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export function getUserDataAndCsrfToken() {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
let i = 0;
|
||||
const waitingOnUserData = setInterval(() => {
|
||||
let userData = null;
|
||||
const dataUserAttribute = document.body.getAttribute('data-user');
|
||||
const meta = document.querySelector("meta[name='csrf-token']");
|
||||
if (dataUserAttribute &&
|
||||
dataUserAttribute !== 'undefined' &&
|
||||
dataUserAttribute !== undefined &&
|
||||
meta &&
|
||||
meta.content !== 'undefined' &&
|
||||
meta.content !== undefined) {
|
||||
userData = JSON.parse(dataUserAttribute);
|
||||
}
|
||||
i += 1;
|
||||
if (userData) {
|
||||
clearInterval(waitingOnUserData);
|
||||
resolve(userData);
|
||||
} else if (i === 3000) {
|
||||
clearInterval(waitingOnUserData);
|
||||
reject(new Error("Couldn't find user data on page."));
|
||||
}
|
||||
}, 5);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
export function scrollToBottom() {
|
||||
const element = document.getElementById('messagelist');
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}
|
||||
22
app/javascript/packs/chat.jsx
Normal file
22
app/javascript/packs/chat.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { h, render } from 'preact';
|
||||
import { getUserDataAndCsrfToken } from '../chat/util';
|
||||
import Chat from '../chat/chat';
|
||||
|
||||
HTMLDocument.prototype.ready = new Promise((resolve) => {
|
||||
if (document.readyState !== 'loading') { return resolve(); }
|
||||
document.addEventListener('DOMContentLoaded', () => resolve());
|
||||
});
|
||||
|
||||
document.ready
|
||||
.then(getUserDataAndCsrfToken()
|
||||
.then((currentUser) => {
|
||||
if (document.getElementById('chat')) {
|
||||
const { pusherKey } = document.getElementById('chat').dataset;
|
||||
window.currentUser = currentUser;
|
||||
window.csrfToken = document.querySelector("meta[name='csrf-token']").content;
|
||||
render(
|
||||
<Chat pusherKey={pusherKey} />,
|
||||
document.getElementById('chat'),
|
||||
);
|
||||
}
|
||||
}));
|
||||
5
app/models/chat_channel.rb
Normal file
5
app/models/chat_channel.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class ChatChannel < ApplicationRecord
|
||||
has_many :messages
|
||||
|
||||
validates :channel_type, presence: true, inclusion: { in: %w(open) }
|
||||
end
|
||||
19
app/models/message.rb
Normal file
19
app/models/message.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
class Message < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :chat_channel
|
||||
|
||||
validates :message_html, presence: true, length: { maximum: 600 }
|
||||
validates :message_markdown, presence: true
|
||||
|
||||
before_validation :evaluate_markdown
|
||||
|
||||
def timestamp
|
||||
created_at.strftime("%H:%M:%S")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def evaluate_markdown
|
||||
self.message_markdown = message_html
|
||||
end
|
||||
end
|
||||
|
|
@ -11,18 +11,19 @@ class User < ApplicationRecord
|
|||
acts_as_followable
|
||||
acts_as_follower
|
||||
|
||||
has_many :articles
|
||||
has_many :reactions
|
||||
belongs_to :organization, optional: true
|
||||
has_many :comments
|
||||
has_many :identities
|
||||
has_many :articles
|
||||
has_many :collections
|
||||
has_many :tweets
|
||||
has_many :notifications
|
||||
has_many :mentions
|
||||
has_many :comments
|
||||
has_many :email_messages, class_name: "Ahoy::Message"
|
||||
has_many :notes
|
||||
has_many :github_repos
|
||||
has_many :identities
|
||||
has_many :mentions
|
||||
has_many :messages
|
||||
has_many :notes
|
||||
has_many :notifications
|
||||
has_many :reactions
|
||||
has_many :tweets
|
||||
|
||||
mount_uploader :profile_image, ProfileImageUploader
|
||||
|
||||
|
|
|
|||
6
app/views/chat_channels/show.json.jbuilder
Normal file
6
app/views/chat_channels/show.json.jbuilder
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
json.messages @chat_channel.messages do |message|
|
||||
json.username message.user.username
|
||||
json.message message.message_markdown
|
||||
json.timestamp message.timestamp
|
||||
json.color message.user.bg_color_hex
|
||||
end
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
<% if user_signed_in? %>
|
||||
<%= javascript_pack_tag 'sendbird', defer: true %>
|
||||
<% if (Flipflop.sendbird?) %>
|
||||
<%= javascript_pack_tag 'sendbird', defer: true %>
|
||||
<% else %>
|
||||
<%= javascript_pack_tag 'chat', defer: true %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% title "DEV LIVE" %>
|
||||
<link rel="canonical" href="https://dev.to/live"/>
|
||||
|
|
@ -31,7 +35,11 @@
|
|||
<script src="//player.dacast.com/js/player.js" width="1500" height="800" id="<%= ENV["DACAST_STREAM_CODE"]%>" player="jw7" jwurl="juQPHW/th8VUHaO/2KkeVlAdGN9ksJZog2z6SH+TyMU=" class="dacast-video"></script>
|
||||
</div>
|
||||
<div class="live-chat-wrapper">
|
||||
<div id="sb_chat" class="live-chat" data-sendbird-app-id="<%= ENV["SENDBIRD_APP_ID"] %>" data-sendbird-livechat-url="<%= ENV["SENDBIRD_LIVECHAT_URL"] %>"></div>
|
||||
<% if (Flipflop.sendbird?) %>
|
||||
<div id="sb_chat" class="live-chat" data-sendbird-app-id="<%= ENV["SENDBIRD_APP_ID"] %>" data-sendbird-livechat-url="<%= ENV["SENDBIRD_LIVECHAT_URL"] %>"></div>
|
||||
<% else %>
|
||||
<div id="chat" class="live-chat" data-pusher-key="<%= ENV["PUSHER_KEY"] %>"</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-under-message">DEV Live is in beta. If you are having issues, you may need to try another browser or contact <a href="mailto:members@dev.to">members@dev.to</a> for support. ❤️</div>
|
||||
|
|
|
|||
|
|
@ -28,4 +28,7 @@ Flipflop.configure do
|
|||
feature :she_coded,
|
||||
default: false,
|
||||
description: "Toggle #shecoded sidebar"
|
||||
feature :sendbird,
|
||||
default: true,
|
||||
description: "Toggle between Sendbird and our custom chat"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ keys = [
|
|||
"BUFFER_LINKEDIN_ID",
|
||||
"BUFFER_PROFILE_ID",
|
||||
"BUFFER_TWITTER_ID",
|
||||
"CLOUDINARY_CLOUD_NAME",
|
||||
"CLOUDINARY_API_KEY",
|
||||
"CLOUDINARY_API_SECRET",
|
||||
"CLOUDINARY_CLOUD_NAME",
|
||||
"CLOUDINARY_SECURE",
|
||||
"DACAST_STREAM_CODE",
|
||||
"DEPLOYMENT_SIGNATURE",
|
||||
|
|
@ -39,8 +39,14 @@ keys = [
|
|||
"MAILCHIMP_NEWSLETTER_ID",
|
||||
"PERIODIC_EMAIL_DIGEST_MAX",
|
||||
"PERIODIC_EMAIL_DIGEST_MIN",
|
||||
"PUSHER_APP_ID",
|
||||
"PUSHER_CLUSTER",
|
||||
"PUSHER_KEY",
|
||||
"PUSHER_SECRET",
|
||||
"RECAPTCHA_SECRET",
|
||||
"RECAPTCHA_SITE",
|
||||
"SENDBIRD_APP_ID",
|
||||
"SENDBIRD_LIVECHAT_URL",
|
||||
"SERVICE_TIMEOUT",
|
||||
"SHARE_MEOW_BASE_URL",
|
||||
"SHARE_MEOW_SECRET_KEY",
|
||||
|
|
@ -51,8 +57,6 @@ keys = [
|
|||
"STREAM_URL",
|
||||
"STRIPE_PUBLISHABLE_KEY",
|
||||
"STRIPE_SECRET_KEY",
|
||||
"SENDBIRD_APP_ID",
|
||||
"SENDBIRD_LIVECHAT_URL",
|
||||
"TWITTER_ACCESS_TOKEN",
|
||||
"TWITTER_ACCESS_TOKEN_SECRET",
|
||||
"TWITTER_KEY",
|
||||
|
|
|
|||
8
config/initializers/pusher.rb
Normal file
8
config/initializers/pusher.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
require "pusher"
|
||||
|
||||
Pusher.app_id = ENV["PUSHER_APP_ID"]
|
||||
Pusher.key = ENV["PUSHER_KEY"]
|
||||
Pusher.secret = ENV["PUSHER_SECRET"]
|
||||
Pusher.cluster = ENV["PUSHER_CLUSTER"]
|
||||
Pusher.logger = Rails.logger
|
||||
Pusher.encrypted = true
|
||||
|
|
@ -57,6 +57,8 @@ Rails.application.routes.draw do
|
|||
resources :reads, only: [:create]
|
||||
end
|
||||
|
||||
resources :messages, only: [:create]
|
||||
resources :chat_channels, only: [:show]
|
||||
resources :articles, only: [:update,:create,:destroy]
|
||||
resources :comments, only:[:create,:update,:destroy]
|
||||
resources :users, only:[:update]
|
||||
|
|
|
|||
9
db/migrate/20180328194237_create_chat_channels.rb
Normal file
9
db/migrate/20180328194237_create_chat_channels.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class CreateChatChannels < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
create_table :chat_channels do |t|
|
||||
t.string :channel_type, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
12
db/migrate/20180328194253_create_messages.rb
Normal file
12
db/migrate/20180328194253_create_messages.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
class CreateMessages < ActiveRecord::Migration[5.1]
|
||||
def change
|
||||
create_table :messages do |t|
|
||||
t.string :message_html, null: false
|
||||
t.string :message_markdown, null: false
|
||||
t.references :user, foreign_key: true, null: false
|
||||
t.references :chat_channel, foreign_key: true, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
48
db/schema.rb
48
db/schema.rb
|
|
@ -10,8 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20180321170500) do
|
||||
|
||||
ActiveRecord::Schema.define(version: 20180328194253) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
|
|
@ -45,6 +44,7 @@ ActiveRecord::Schema.define(version: 20180321170500) do
|
|||
t.boolean "automatically_renew", default: false
|
||||
t.text "body_html"
|
||||
t.text "body_markdown"
|
||||
t.boolean "boosted", default: false
|
||||
t.string "cached_tag_list"
|
||||
t.string "cached_user_name"
|
||||
t.string "cached_user_username"
|
||||
|
|
@ -68,6 +68,7 @@ ActiveRecord::Schema.define(version: 20180321170500) do
|
|||
t.integer "job_opportunity_id"
|
||||
t.string "language"
|
||||
t.datetime "last_buffered"
|
||||
t.datetime "last_comment_at", default: "2017-01-01 05:00:00"
|
||||
t.datetime "last_invoiced_at"
|
||||
t.decimal "lat", precision: 10, scale: 6
|
||||
t.boolean "live_now", default: false
|
||||
|
|
@ -105,8 +106,6 @@ ActiveRecord::Schema.define(version: 20180321170500) do
|
|||
t.string "video_code"
|
||||
t.string "video_source_url"
|
||||
t.string "video_thumbnail_url"
|
||||
t.datetime "last_comment_at", default: "2017-01-01 05:00:00"
|
||||
t.boolean "boosted", default: false
|
||||
t.index ["featured_number"], name: "index_articles_on_featured_number"
|
||||
t.index ["hotness_score"], name: "index_articles_on_hotness_score"
|
||||
t.index ["published_at"], name: "index_articles_on_published_at"
|
||||
|
|
@ -143,6 +142,12 @@ ActiveRecord::Schema.define(version: 20180321170500) do
|
|||
t.string "type_of"
|
||||
end
|
||||
|
||||
create_table "chat_channels", force: :cascade do |t|
|
||||
t.string "channel_type", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "collections", id: :serial, force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.string "description"
|
||||
|
|
@ -216,19 +221,19 @@ ActiveRecord::Schema.define(version: 20180321170500) do
|
|||
end
|
||||
|
||||
create_table "events", force: :cascade do |t|
|
||||
t.string "title"
|
||||
t.string "category"
|
||||
t.datetime "starts_at"
|
||||
t.string "cover_image"
|
||||
t.datetime "created_at", null: false
|
||||
t.text "description_html"
|
||||
t.text "description_markdown"
|
||||
t.datetime "ends_at"
|
||||
t.string "location_name"
|
||||
t.string "location_url"
|
||||
t.string "cover_image"
|
||||
t.text "description_markdown"
|
||||
t.text "description_html"
|
||||
t.boolean "published"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "slug"
|
||||
t.datetime "starts_at"
|
||||
t.string "title"
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "feedback_messages", force: :cascade do |t|
|
||||
|
|
@ -319,6 +324,17 @@ ActiveRecord::Schema.define(version: 20180321170500) do
|
|||
t.integer "user_id"
|
||||
end
|
||||
|
||||
create_table "messages", force: :cascade do |t|
|
||||
t.bigint "chat_channel_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.string "message_html", null: false
|
||||
t.string "message_markdown", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.bigint "user_id", null: false
|
||||
t.index ["chat_channel_id"], name: "index_messages_on_chat_channel_id"
|
||||
t.index ["user_id"], name: "index_messages_on_user_id"
|
||||
end
|
||||
|
||||
create_table "notes", id: :serial, force: :cascade do |t|
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
|
|
@ -358,13 +374,13 @@ ActiveRecord::Schema.define(version: 20180321170500) do
|
|||
t.string "state"
|
||||
t.string "story"
|
||||
t.text "summary"
|
||||
t.string "tag_line"
|
||||
t.string "tech_stack"
|
||||
t.string "text_color_hex"
|
||||
t.string "twitter_username"
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "url"
|
||||
t.string "zip_code"
|
||||
t.string "tag_line"
|
||||
t.index ["slug"], name: "index_organizations_on_slug", unique: true
|
||||
end
|
||||
|
||||
|
|
@ -410,9 +426,9 @@ ActiveRecord::Schema.define(version: 20180321170500) do
|
|||
t.text "status_notice", default: ""
|
||||
t.string "title"
|
||||
t.string "twitter_username"
|
||||
t.boolean "unique_website_url?", default: true
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "website_url"
|
||||
t.boolean "unique_website_url?", default: true
|
||||
end
|
||||
|
||||
create_table "reactions", id: :serial, force: :cascade do |t|
|
||||
|
|
@ -534,6 +550,7 @@ ActiveRecord::Schema.define(version: 20180321170500) do
|
|||
t.string "education"
|
||||
t.string "email", default: "", null: false
|
||||
t.boolean "email_comment_notifications", default: true
|
||||
t.boolean "email_digest_periodic", default: true, null: false
|
||||
t.boolean "email_follower_notifications", default: true
|
||||
t.boolean "email_membership_newsletter", default: false
|
||||
t.boolean "email_mention_notifications", default: true
|
||||
|
|
@ -573,6 +590,7 @@ ActiveRecord::Schema.define(version: 20180321170500) do
|
|||
t.boolean "onboarding_package_requested_again", default: false
|
||||
t.boolean "org_admin", default: false
|
||||
t.integer "organization_id"
|
||||
t.boolean "permit_adjacent_sponsors", default: true
|
||||
t.datetime "personal_data_updated_at"
|
||||
t.string "profile_image"
|
||||
t.integer "reactions_count", default: 0, null: false
|
||||
|
|
@ -617,8 +635,6 @@ ActiveRecord::Schema.define(version: 20180321170500) do
|
|||
t.string "username"
|
||||
t.string "website_url"
|
||||
t.datetime "workshop_expiration"
|
||||
t.boolean "permit_adjacent_sponsors", default: true
|
||||
t.boolean "email_digest_periodic", default: true, null: false
|
||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||
t.index ["language_settings"], name: "index_users_on_language_settings", using: :gin
|
||||
t.index ["organization_id"], name: "index_users_on_organization_id"
|
||||
|
|
@ -632,4 +648,6 @@ ActiveRecord::Schema.define(version: 20180321170500) do
|
|||
t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id"
|
||||
end
|
||||
|
||||
add_foreign_key "messages", "chat_channels"
|
||||
add_foreign_key "messages", "users"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||
"preact": "^8.2.5",
|
||||
"prop-types": "^15.6.0",
|
||||
"pusher-js": "^4.2.2",
|
||||
"sendbird": "^3.0.52"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
spec/factories/chat_channels.rb
Normal file
5
spec/factories/chat_channels.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
FactoryBot.define do
|
||||
factory :chat_channel do
|
||||
channel_type { "open" }
|
||||
end
|
||||
end
|
||||
0
spec/factories/messages.rb
Normal file
0
spec/factories/messages.rb
Normal file
6
spec/models/chat_channel_spec.rb
Normal file
6
spec/models/chat_channel_spec.rb
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe ChatChannel, type: :model do
|
||||
it { is_expected.to have_many(:messages) }
|
||||
it { is_expected.to validate_presence_of(:channel_type) }
|
||||
end
|
||||
8
spec/models/message_spec.rb
Normal file
8
spec/models/message_spec.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Message, type: :model do
|
||||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to belong_to(:chat_channel) }
|
||||
it { is_expected.to validate_presence_of(:message_html) }
|
||||
it { is_expected.to validate_presence_of(:message_markdown) }
|
||||
end
|
||||
33
spec/requests/chat_channels_spec.rb
Normal file
33
spec/requests/chat_channels_spec.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "ChatChannels", type: :request do
|
||||
describe "GET /chat_channels/:id" do
|
||||
context "when request is valid" do
|
||||
let(:chat_channel) { create(:chat_channel) }
|
||||
|
||||
before do
|
||||
get "/chat_channels/#{chat_channel.id}", headers: { HTTP_ACCEPT: "application/json" }
|
||||
end
|
||||
|
||||
it "returns 200" do
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it "returns the channel" do
|
||||
expect(response).to render_template(:show)
|
||||
end
|
||||
end
|
||||
|
||||
context "when request is invalid" do
|
||||
before { get "/chat_channels/1" }
|
||||
|
||||
it "returns proper error message" do
|
||||
expect(response.body).to include("invalid")
|
||||
end
|
||||
|
||||
it "returns 401" do
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
50
spec/requests/messages_spec.rb
Normal file
50
spec/requests/messages_spec.rb
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Messages", type: :request do
|
||||
let(:user) { create(:user) }
|
||||
let(:chat_channel) { create(:chat_channel) }
|
||||
|
||||
describe "POST /messages" do
|
||||
let(:new_message) do
|
||||
{
|
||||
message_html: "hi",
|
||||
user_id: user.id,
|
||||
chat_channel_id: chat_channel.id,
|
||||
}
|
||||
end
|
||||
|
||||
it "requires user to be signed in" do
|
||||
post "/messages", params: { message: new_message }
|
||||
expect(response.status).to eq(302)
|
||||
end
|
||||
# Pusher::Error
|
||||
|
||||
context "when user is signed in" do
|
||||
before do
|
||||
allow(Pusher).to receive(:trigger).and_return(true)
|
||||
sign_in user
|
||||
post "/messages", params: { message: new_message }
|
||||
end
|
||||
|
||||
it "returns 201 upon success" do
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
it "returns in json" do
|
||||
expect(response.content_type).to eq("application/json")
|
||||
end
|
||||
end
|
||||
|
||||
context "when Pusher isn't cooperating" do
|
||||
before do
|
||||
allow(Pusher).to receive(:trigger).and_raise(Pusher::Error)
|
||||
sign_in user
|
||||
post "/messages", params: { message: new_message }
|
||||
end
|
||||
|
||||
it "returns proper message" do
|
||||
expect(response.body).to include("could not trigger Pusher")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
yarn.lock
17
yarn.lock
|
|
@ -3462,6 +3462,12 @@ fastparse@^1.1.1:
|
|||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
|
||||
|
||||
faye-websocket@0.9.4:
|
||||
version "0.9.4"
|
||||
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.9.4.tgz#885934c79effb0409549e0c0a3801ed17a40cdad"
|
||||
dependencies:
|
||||
websocket-driver ">=0.5.1"
|
||||
|
||||
faye-websocket@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
|
||||
|
|
@ -6893,6 +6899,13 @@ punycode@^2.1.0:
|
|||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
|
||||
|
||||
pusher-js@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/pusher-js/-/pusher-js-4.2.2.tgz#de391bfb14d221ef96f462304f8a73b95ce9acc5"
|
||||
dependencies:
|
||||
faye-websocket "0.9.4"
|
||||
xmlhttprequest "^1.8.0"
|
||||
|
||||
q@^1.1.2:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
|
||||
|
|
@ -8819,6 +8832,10 @@ xml-name-validator@^2.0.1:
|
|||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
|
||||
|
||||
xmlhttprequest@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
|
||||
|
||||
xtend@^4.0.0, xtend@~4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue