Implement chatroom (#308)

* Fix multiple chat loading bug

* Open profile links in chat via new tab

* Add unmount handling for chat

* Add overscroll property to chat

* Add channel_name to ChatChannel model

* Create route and page for chatroom WIP

* Expose ChatChannel id to live page

* Implement multichannel support WIP

* Implement chatroom page WIP

* Add css for chatroom

* Update chat's moderation

* Refactor chat's message type

* Swap the use of :message_markdown & :message_html

* Add channel permissions WIP

* Fix failing specs

* Adjust json reponses

* Add empty message prevention

* Update participant error message

* Change Workshop channel to General
This commit is contained in:
Mac Siri 2018-05-14 12:50:32 -04:00 committed by Ben Halpern
parent b2c7450c6f
commit b4292ade33
25 changed files with 503 additions and 98 deletions

View file

@ -1,5 +1,27 @@
// High level class
.chatchannel {
.chat {
display: flex;
height: inherit;
}
.chat__channels {
width: 125px;
border: 1px solid #c9c9c9;
border-right: none;
background: #ededed;
height: inherit;
}
.chat__channels--hidden {
display: none;
}
.chat__activechat {
flex-grow: 1;
height: inherit;
}
.activechatchannel {
height: inherit;
max-height: inherit;
display: flex;
@ -8,7 +30,7 @@
border: 1px solid #c9c9c9;
}
.chatchannel__messages {
.activechatchannel__messages {
font-size: 15px;
padding: 10px 0px;
display: flex;
@ -20,9 +42,10 @@
text-align: left;
overflow-wrap: break-word;
word-wrap: break-word;
overscroll-behavior-y: contain;
}
.chatchannel__alerts {
.activechatchannel__alerts {
position: relative;
}
@ -43,7 +66,7 @@
display: none;
}
.chatchannel__form {
.activechatchannel__form {
border-top: 1px solid #c9c9c9;
background: #ededed;
width: 100%;
@ -61,10 +84,67 @@
}
.chatchannels {
display: flex;
flex-direction: column;
justify-content: space-between;
height: inherit;
}
.chatchanneltab {
height: 30px;
width: 100%;
margin-bottom: 5px;
border: none;
border-left: 8px solid transparent;
background: inherit;
text-align: start;
text-transform: uppercase;
font-size: 14px;
font-weight: 500;
}
.chatchanneltab--active {
background: white;
border-left: 8px solid black;
}
.chatchanneltab--inactive {
&:hover {
background: #e3e3e3;
}
}
@keyframes example {
// animation-name: example;
// animation-duration: 4s;
// animation-iteration-count: infinite;
from {
border-left: 8px solid transparent;
}
to {
border-left: 8px solid #ffb6c1;
}
}
.chatchannels__channelslist {
padding: 10px 0;
flex-grow: 1;
}
.chatchannels__misc {
height: 70px;
border-top: 1px solid #c9c9c9
}
.chatmessage__username {
font-weight: 600;
}
.chatmessage__username--link {
color: inherit;
}
.chatmessage__divider {
}
@ -73,6 +153,13 @@
}
.chatmessage__currentuser {
background: black;
color: white;
padding: 1px 5px;
border-radius: 2px;
}
// Messagecomposer
.messagecomposer {
display: flex;
@ -87,7 +174,7 @@
margin: 10px;
padding: 6px;
resize: none;
width: 80%;
flex-grow: 1;
}
.messagecomposer__submit {
@ -96,4 +183,5 @@
border-radius: 3px;
background: #66e2d5;
font-weight: 500px;
width: 50px;
}

View file

@ -23,22 +23,23 @@ class ChatChannelsController < ApplicationController
banned_user.add_role :banned
banned_user.messages.each(&:destroy!)
Pusher.trigger(@chat_channel.id, "user-banned", { userId: banned_user.id }.to_json)
render json: { success: "banned!" }, status: 200
render json: { status: "success", message: "banned!" }, status: 200
else
render json: { error: "username not found" }, status: 400
render json: { status: "error", message: "username not found" }, status: 400
end
when "/unban"
banned_user = User.find_by_username(command[1])
if banned_user
banned_user.remove_role :banned
render json: { success: "unbanned!" }, status: 200
render json: { status: "success", message: "unbanned!" }, status: 200
else
render json: { error: "username not found" }, status: 400
render json: { status: "error", message: "username not found" }, status: 400
end
when "/clearchannel"
@chat_channel.clear_channel
render json: { status: "success", message: "cleared!" }, status: 200
else
render json: { error: "invalid command" }, status: 400
render json: { status: "error", message: "invalid command" }, status: 400
end
end

View file

@ -16,13 +16,20 @@ class MessagesController < ApplicationController
end
if success
render json: ["Message created"], status: 201
render json: { status: "success", message: "Message created" }, status: 201
else
result = "Message created but could not trigger Pusher"
render json: [result, @message.to_json], status: 201
error_message = "Message created but could not trigger Pusher"
render json: { status: "error", message: error_message }, status: 201
end
else
render json: e.message, status: 401
render json: {
status: "error",
message: {
chat_channel_id: @message.chat_channel_id,
message: @message.errors.full_messages,
type: "error",
},
}, status: 401
end
end
@ -31,14 +38,30 @@ class MessagesController < ApplicationController
def create_pusher_payload(new_message)
{
user_id: new_message.user.id,
chat_channel_id: new_message.chat_channel.id,
username: new_message.user.username,
message: new_message.message_markdown,
message: new_message.message_html,
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)
params.require(:message).permit(:message_markdown, :user_id, :chat_channel_id)
end
def user_not_authorized
respond_to do |format|
format.json do
render json: {
status: "error",
message: {
chat_channel_id: message_params[:chat_channel_id],
message: "You can not do that because you are banned",
type: "error",
},
}, status: 401
end
end
end
end

View file

@ -38,7 +38,15 @@ class PagesController < ApplicationController
end
end
def live; end
def live
@chat_channels = [ChatChannel.find_by_channel_name("Workshop")].to_json
end
def chat
workshop = ChatChannel.find_by_channel_name("General")
meta = ChatChannel.find_by_channel_name("Meta")
@chat_channels = [workshop, meta].to_json
end
private # helpers

View file

@ -1,5 +1,5 @@
export function getAllMessages(successCb, failureCb) {
fetch('/chat_channels/1', {
export function getAllMessages(channelId, successCb, failureCb) {
fetch(`/chat_channels/${channelId}`, {
Accept: 'application/json',
'Content-Type': 'application/json',
})
@ -8,7 +8,7 @@ export function getAllMessages(successCb, failureCb) {
.catch(failureCb);
}
export function sendMessage(message, successCb, failureCb) {
export function sendMessage(activeChannelId, message, successCb, failureCb) {
fetch('/messages', {
method: 'POST',
headers: {
@ -18,9 +18,9 @@ export function sendMessage(message, successCb, failureCb) {
},
body: JSON.stringify({
message: {
message_html: message,
message_markdown: message,
user_id: window.currentUser.id,
chat_channel_id: '1',
chat_channel_id: activeChannelId,
},
}),
credentials: 'same-origin',
@ -30,8 +30,13 @@ export function sendMessage(message, successCb, failureCb) {
.catch(failureCb);
}
export function conductModeration(message, successCb, failureCb) {
fetch('/chat_channels/1/moderate', {
export function conductModeration(
activeChannelId,
message,
successCb,
failureCb,
) {
fetch(`/chat_channels/${activeChannelId}/moderate`, {
method: 'POST',
headers: {
Accept: 'application/json',

View file

@ -0,0 +1,34 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
const Channels = ({ activeChannelId, chatChannels, handleSwitchChannel }) => {
const channels = chatChannels.map(channel => {
const otherClassname =
parseInt(activeChannelId, 10) === channel.id
? 'chatchanneltab--active'
: 'chatchanneltab--inactive';
return (
<button
className={`chatchanneltab ${otherClassname}`}
onClick={handleSwitchChannel}
data-channel-id={channel.id}
>
{channel.channel_name}
</button>
);
});
return (
<div className="chatchannels">
<div className="chatchannels__channelslist">{channels}</div>
</div>
);
};
Channels.propTypes = {
activeChannelId: PropTypes.number.isRequired,
chatChannels: PropTypes.array.isRequired,
handleSwitchChannel: PropTypes.func.isRequired,
};
export default Channels;

View file

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { conductModeration, getAllMessages, sendMessage } from './actions';
import { hideMessages, scrollToBottom, setupObserver } from './util';
import Alert from './alert';
import Channels from './channels';
import Compose from './compose';
import Message from './message';
import setupPusher from './pusher';
@ -20,19 +21,25 @@ class Chat extends Component {
this.receiveNewMessage = this.receiveNewMessage.bind(this);
this.clearChannel = this.clearChannel.bind(this);
this.redactUserMessages = this.redactUserMessages.bind(this);
this.handleSwitchChannel = this.handleSwitchChannel.bind(this);
const chatChannels = JSON.parse(this.props.chatChannels);
const chatOptions = JSON.parse(this.props.chatOptions);
this.state = {
messages: [],
messages: chatChannels.reduce(
(accumulator, target) => ({ ...accumulator, [target.id]: [] }),
{},
),
scrolled: false,
showAlert: false,
chatChannels,
activeChannelId: chatChannels[0].id,
showChannelsList: chatOptions.showChannelsList,
};
}
componentDidMount() {
getAllMessages(this.receiveAllMessages);
setupPusher(this.props.pusherKey, {
messageCreated: this.receiveNewMessage,
channelCleared: this.clearChannel,
redactUserMessages: this.redactUserMessages,
this.state.chatChannels.forEach(channel => {
this.setupChannel(channel.id);
});
setupObserver(this.observerCallback);
}
@ -43,8 +50,18 @@ class Chat extends Component {
}
}
setupChannel(channelId) {
getAllMessages(channelId, this.receiveAllMessages);
setupPusher(this.props.pusherKey, {
channelId,
messageCreated: this.receiveNewMessage,
channelCleared: this.clearChannel,
redactUserMessages: this.redactUserMessages,
});
}
observerCallback(entries) {
entries.forEach((entry) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.setState({ scrolled: false, showAlert: false });
} else {
@ -54,57 +71,93 @@ class Chat extends Component {
}
receiveAllMessages(res) {
this.setState({ messages: res.messages });
const { chatChannelId, messages } = res;
const newMessages = { ...this.state.messages, [chatChannelId]: messages };
this.setState({ messages: newMessages });
}
receiveNewMessage(message) {
const newMessages = this.state.messages.slice();
const receivedChatChannelId = message.chat_channel_id;
const newMessages = this.state.messages[receivedChatChannelId].slice();
newMessages.push(message);
if (newMessages.length > 150) {
newMessages.shift();
}
const newShowAlert =
this.state.activeChannelId === receivedChatChannelId
? { showAlert: this.state.scrolled }
: {};
this.setState({
messages: newMessages,
showAlert: this.state.scrolled,
...newShowAlert,
messages: {
...this.state.messages,
[receivedChatChannelId]: newMessages,
},
});
}
redactUserMessages(res) {
const newMessages = hideMessages(this.state.messages.slice(), res.userId);
// This is shallow clone. This might cause a problem
const clonedMessages = Object.assign({}, this.state.messages);
const newMessages = hideMessages(clonedMessages, res.userId);
this.setState({ messages: newMessages });
}
clearChannel() {
this.setState({ messages: [] });
clearChannel(res) {
const newMessages = { ...this.state.messages, [res.chat_channel_id]: [] };
this.setState({ messages: newMessages });
}
handleKeyDown(e) {
if (e.keyCode === 13) {
e.preventDefault();
this.handleMessageSubmit(e.target.value);
e.target.value = '';
if (e.target.value.length > 0) {
this.handleMessageSubmit(e.target.value);
e.target.value = '';
}
}
}
handleMessageSubmit(message) {
// should check if user has the priviledge
if (message[0] === '/') {
conductModeration(message, this.handleSuccess, this.handleFailure);
conductModeration(
this.state.activeChannelId,
message,
this.handleSuccess,
this.handleFailure,
);
} else {
sendMessage(message, this.handleSuccess, this.handleFailure);
sendMessage(
this.state.activeChannelId,
message,
this.handleSuccess,
this.handleFailure,
);
}
}
handleSwitchChannel(e) {
e.preventDefault();
this.setState({
activeChannelId: e.target.dataset.channelId,
scrolled: false,
showAlert: false,
});
}
handleSubmitOnClick(e) {
e.preventDefault();
const message = document.getElementById('messageform').value;
this.handleMessageSubmit(message);
document.getElementById('messageform').value = '';
if (message.length > 0) {
this.handleMessageSubmit(message);
document.getElementById('messageform').value = '';
}
}
handleSuccess(response) {
if (Object.prototype.hasOwnProperty.call(response, 'error')) {
console.log(response.error);
if (response.status === 'error') {
this.receiveNewMessage(response.message);
}
}
@ -113,28 +166,44 @@ class Chat extends Component {
}
renderMessage() {
return this.state.messages.map(message => (
const { activeChannelId, messages } = this.state;
return messages[activeChannelId].map(message => (
<Message
user={message.username}
message={message.message}
timeStamp={message.timestamp}
color={message.color}
hidden={message.hidden}
type={message.type}
/>
));
}
render() {
renderChatChannels() {
if (this.state.showChannelsList) {
return (
<div className="chat__channels">
<Channels
activeChannelId={this.state.activeChannelId}
chatChannels={this.state.chatChannels}
handleSwitchChannel={this.handleSwitchChannel}
/>
</div>
);
}
return '';
}
renderActiveChatChannel() {
return (
<div className="chatchannel">
<div className="chatchannel__messages" id="messagelist">
<div className="activechatchannel">
<div className="activechatchannel__messages" id="messagelist">
{this.renderMessage()}
<div id="messagelist__sentinel" />
</div>
<div className="chatchannel__alerts">
<div className="activechatchannel__alerts">
<Alert showAlert={this.state.showAlert} />
</div>
<div className="chatchannel__form">
<div className="activechatchannel__form">
<Compose
handleKeyDown={this.handleKeyDown}
handleSubmitOnClick={this.handleSubmitOnClick}
@ -143,10 +212,21 @@ class Chat extends Component {
</div>
);
}
render() {
return (
<div className="chat">
{this.renderChatChannels()}
<div className="chat__activechat">{this.renderActiveChatChannel()}</div>
</div>
);
}
}
Chat.propTypes = {
pusherKey: PropTypes.number.isRequired,
chatChannels: PropTypes.array.isRequired,
chatOptions: PropTypes.object.isRequired,
};
export default Chat;

View file

@ -1,28 +1,53 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
import ErrorMessage from './messages/errorMessage';
import Hiddenmessage from './messages/hiddenMessage';
/*
* The prop also contain timeStamp, which is currently not in used
*
*/
const Message = ({
user, message, color, hidden,
}) => {
const Message = ({ user, message, color, type }) => {
const spanStyle = { color };
const linkStyle = { color: 'inherit' };
const messageStyle = { color: hidden ? 'lightgray' : 'inherit' };
if (type === 'error') {
return <ErrorMessage message={message} />;
} else if (type === 'hidden') {
return <Hiddenmessage user={user} color={color} />;
}
const re = new RegExp(`@${window.currentUser.username}`);
const match = re.exec(message);
let messageArea;
if (match) {
messageArea = (
<span className="chatmessage__message">
{message.substr(0, match.index)}
<span className="chatmessage__currentuser">
{`@${window.currentUser.username}`}
</span>
{message.substr(match.index + match[0].length)}
</span>
);
} else {
messageArea = <span className="chatmessage__message">{message}</span>;
}
return (
<div className="chatmessage">
<span className="chatmessage__username" style={spanStyle}>
<a style={linkStyle} href={`/${user}`}>
<a
className="chatmessage__username--link"
href={`/${user}`}
target="_blank"
>
{user}
</a>
</span>
<span className="chatmessage__divider">: </span>
<span className="chatmessage__message" style={messageStyle}>
{hidden ? '<message removed>' : message}
</span>
{messageArea}
</div>
);
};
@ -31,11 +56,13 @@ Message.propTypes = {
user: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
hidden: PropTypes.bool,
type: PropTypes.string,
// hidden: PropTypes.bool,
// error: PropTypes.bool,
};
Message.defaultProps = {
hidden: false,
type: 'normalMessage',
};
export default Message;

View file

@ -0,0 +1,23 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
const ErrorMessage = ({ message }) => {
const errorStyle = { color: 'darksalmon', 'font-size': '13px' };
return (
<div className="chatmessage">
<span className="chatmessage__message" style={errorStyle}>
{'Sorry '}
<span className="chatmessage__currentuser">
{`@${window.currentUser.username}`}
</span>
{` ${message}`}
</span>
</div>
);
};
ErrorMessage.propTypes = {
message: PropTypes.string.isRequired,
};
export default ErrorMessage;

View file

@ -0,0 +1,30 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
const HiddenMessage = ({ user, color }) => {
const spanStyle = { color };
return (
<div className="chatmessage">
<span className="chatmessage__username" style={spanStyle}>
<a
className="chatmessage__username--link"
href={`/${user}`}
target="_blank"
>
{user}
</a>
</span>
<span className="chatmessage__divider">: </span>
<span className="chatmessage__message" style={{ color: 'lightgray' }}>
{'<message removed>'}
</span>
</div>
);
};
HiddenMessage.propTypes = {
user: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
};
export default HiddenMessage;

View file

@ -6,7 +6,7 @@ export default function setupPusher(key, callbackObjects) {
encrypted: true,
});
const channel = pusher.subscribe('1');
const channel = pusher.subscribe(callbackObjects.channelId.toString());
channel.bind('message-created', callbackObjects.messageCreated);
channel.bind('channel-cleared', callbackObjects.channelCleared);
channel.bind('user-banned', callbackObjects.redactUserMessages);

View file

@ -40,11 +40,17 @@ export function setupObserver(callback) {
}
export function hideMessages(messages, userId) {
const newMessages = messages.map((message) => {
if (message.user_id === userId) {
return Object.assign({ hidden: true }, message);
}
return message;
});
return newMessages;
const cleanedMessages = Object.keys(messages).reduce(
(accumulator, channelId) => {
const newMessages = messages[channelId].map(message => {
if (message.user_id === userId) {
return Object.assign({ type: 'hidden' }, message);
}
return message;
});
return { ...accumulator, [channelId]: newMessages };
},
{},
);
return cleanedMessages;
}

View file

@ -2,21 +2,35 @@ 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(); }
HTMLDocument.prototype.ready = new Promise(resolve => {
if (document.readyState !== 'loading') {
return resolve();
}
document.addEventListener('DOMContentLoaded', () => resolve());
return null;
});
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'),
);
}
}));
document.ready.then(
getUserDataAndCsrfToken().then(currentUser => {
if (document.getElementById('chat')) {
const { chatChannels, pusherKey, chatOptions } = document.getElementById(
'chat',
).dataset;
window.currentUser = currentUser;
window.csrfToken = document.querySelector(
"meta[name='csrf-token']",
).content;
const root = render(
<Chat
pusherKey={pusherKey}
chatChannels={chatChannels}
chatOptions={chatOptions}
/>,
document.getElementById('chat'),
);
window.InstantClick.on('change', () => {
render('', document.body, root);
});
}
}),
);

View file

@ -1,11 +1,12 @@
class ChatChannel < ApplicationRecord
has_many :messages
validates :channel_type, presence: true, inclusion: { in: %w(open) }
validates :channel_type, presence: true, inclusion: { in: %w(open invite_only) }
def clear_channel
messages.each(&:destroy!)
Pusher.trigger(id, "channel-cleared", [].to_json)
Pusher.trigger(id, "channel-cleared", { chat_channel_id: id }.to_json)
true
rescue Pusher::Error => e
logger.info "PUSHER ERROR: #{e.message}"
end

View file

@ -2,10 +2,11 @@ class Message < ApplicationRecord
belongs_to :user
belongs_to :chat_channel
validates :message_html, presence: true, length: { maximum: 600 }
validates :message_markdown, presence: true
validates :message_html, presence: true
validates :message_markdown, presence: true, length: { maximum: 600 }
before_validation :evaluate_markdown
before_validation :evaluate_channel_permission
def timestamp
created_at.strftime("%H:%M:%S")
@ -14,6 +15,16 @@ class Message < ApplicationRecord
private
def evaluate_markdown
self.message_markdown = message_html
self.message_html = message_markdown
end
def evaluate_channel_permission
channel_type = ChatChannel.find(chat_channel_id).channel_type
return if channel_type == "open"
if user&.has_role?(:chatroom_beta_tester)
# this is fine
else
errors.add(:base, "You are not a participant of this chat channel.")
end
end
end

View file

@ -26,6 +26,7 @@ class Role < ApplicationRecord
level_1_member
workshop_pass
video_permission
chatroom_beta_tester
),
}
scopify

View file

@ -1,3 +1,4 @@
json.messages @chat_channel.messages.order("created_at DESC").limit(50).reverse do |message|
json.user_id message.user.id
json.username message.user.username
@ -5,3 +6,7 @@ json.messages @chat_channel.messages.order("created_at DESC").limit(50).reverse
json.timestamp message.timestamp
json.color message.user.bg_color_hex
end
json.key_format! camelize: :lower
json.chat_channel_id @chat_channel.id

View file

@ -0,0 +1,24 @@
<% if user_signed_in? %>
<%= javascript_pack_tag 'chat', defer: true %>
<% end %>
<% title "DEV Chat" %>
<link rel="canonical" href="https://dev.to/chat"/>
<meta name="description" content="DEV Chat">
<style>
.inner-content {
height: 500px;
margin: 0px 50px;
margin-top: 150px;
margin-bottom: 150px;
max-width: 1250px;
}
.live-chat {
height: 500px;
}
</style>
<div class="inner-content">
<div id="chat" class="live-chat" data-pusher-key="<%= ENV["PUSHER_KEY"] %>" data-chat-channels="<%= @chat_channels %>" data-chat-options="<%= {showChannelsList:true}.to_json %>">
</div>

View file

@ -39,7 +39,7 @@
<% 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>
<div id="chat" class="live-chat" data-pusher-key="<%= ENV["PUSHER_KEY"] %>" data-chat-channels="<%= @chat_channels %>" data-chat-options="<%= {showChannelsList:false}.to_json %>">
<% end %>
</div>
</div>

View file

@ -148,6 +148,7 @@ Rails.application.routes.draw do
get "/infiniteloop" => "pages#infinite_loop"
get "/faq" => "pages#faq"
get "/live" => "pages#live"
get "/chat" => "pages#chat"
get "/swagnets" => "pages#swagnets"
get "/welcome" => "pages#welcome"
get "/💸", to: redirect("t/hiring")

View file

@ -0,0 +1,5 @@
class AddChannelNameToChatChannels < ActiveRecord::Migration[5.1]
def change
add_column :chat_channels, :channel_name, :string
end
end

View file

@ -161,6 +161,7 @@ ActiveRecord::Schema.define(version: 20180508200948) do
end
create_table "chat_channels", force: :cascade do |t|
t.string "channel_name"
t.string "channel_type", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false

View file

@ -0,0 +1,13 @@
FactoryBot.define do
factory :message do
message_markdown { Faker::Lorem.sentence }
chat_channel
trait :ignore_after_callback do
after(:build) do |message|
message.define_singleton_method(:evaluate_channel_permission) {}
message.define_singleton_method(:evaluate_markdown) {}
end
end
end
end

View file

@ -1,8 +1,12 @@
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) }
describe "validations" do
subject { build(:message, :ignore_after_callback)}
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
end

View file

@ -7,7 +7,7 @@ RSpec.describe "Messages", type: :request do
describe "POST /messages" do
let(:new_message) do
{
message_html: "hi",
message_markdown: "hi",
user_id: user.id,
chat_channel_id: chat_channel.id,
}