Update chat app's features (#204)
* Update chat.scss --skip-ci * Fix height issue * Add highlighting * Implement bannged user handling for chat * Add scroll obsever * Limit initial messages load count & max messages * Add new message alert when scrolled * Add link to usernames in chat * Implement channel clearing feature * Update parserOptions * Implement banning feature for chat app WIP * Add policy for ChatChannelsController * Fix arugment error * Update user_not_authorized to handle JSON request * Update specs for updated user_not_authorized * Update font-weight for windows os
This commit is contained in:
parent
610ae3da22
commit
0f35939906
22 changed files with 371 additions and 93 deletions
|
|
@ -1,6 +1,7 @@
|
|||
// High level class
|
||||
.chatchannel {
|
||||
height: inherit;
|
||||
max-height: inherit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
|
@ -9,7 +10,7 @@
|
|||
|
||||
.chatchannel__messages {
|
||||
font-size: 15px;
|
||||
padding: 10px;
|
||||
padding: 10px 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
|
@ -17,6 +18,29 @@
|
|||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
text-align: left;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.chatchannel__alerts {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chatalert__default {
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
display: flex;
|
||||
height: 25px;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.chatalert__default--hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chatchannel__form {
|
||||
|
|
@ -29,11 +53,16 @@
|
|||
// Chatmessage
|
||||
.chatmessage {
|
||||
display: inline-block;
|
||||
margin-bottom: 3px;
|
||||
padding: 3px 10px;
|
||||
|
||||
&:hover {
|
||||
background: #e3e3e3;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chatmessage__username {
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chatmessage__divider {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
.live-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin:auto;
|
||||
max-width:1636px;
|
||||
min-height:96vh;
|
||||
|
|
@ -7,36 +9,38 @@
|
|||
margin-top:78px;
|
||||
width: 98%;
|
||||
}
|
||||
.live-video{
|
||||
width: 100%;
|
||||
height:62.5vw;
|
||||
text-align:center;
|
||||
max-height:920px;
|
||||
float:left;
|
||||
@media screen and ( min-width:760px ){
|
||||
width: calc(100% - 300px);
|
||||
height: calc(100vh - 132px);
|
||||
}
|
||||
@media screen and ( min-width:1200px ){
|
||||
width: calc(100% - 400px);
|
||||
}
|
||||
}
|
||||
.live-chat-wrapper{
|
||||
text-align:center;
|
||||
float:left;
|
||||
width: 100%;
|
||||
.live-chat{
|
||||
height: calc(100vh - 62.5vw - 42px);
|
||||
}
|
||||
@media screen and ( min-width: 760px ){
|
||||
width:300px;
|
||||
.live-chat{
|
||||
height: calc(100vh - 134px);
|
||||
max-height:920px;
|
||||
.live-component {
|
||||
.live-video{
|
||||
width: 100%;
|
||||
height:62.5vw;
|
||||
text-align:center;
|
||||
max-height:920px;
|
||||
float:left;
|
||||
@media screen and ( min-width:760px ){
|
||||
width: calc(100% - 300px);
|
||||
height: calc(100vh - 132px);
|
||||
}
|
||||
@media screen and ( min-width:1200px ){
|
||||
width: calc(100% - 400px);
|
||||
}
|
||||
}
|
||||
@media screen and ( min-width: 1200px ){
|
||||
width:400px;
|
||||
.live-chat-wrapper{
|
||||
text-align:center;
|
||||
float:left;
|
||||
width: 100%;
|
||||
.live-chat{
|
||||
height: calc(100vh - 62.5vw - 42px);
|
||||
}
|
||||
@media screen and ( min-width: 760px ){
|
||||
width:300px;
|
||||
.live-chat{
|
||||
height: calc(100vh - 134px);
|
||||
max-height:920px;
|
||||
}
|
||||
}
|
||||
@media screen and ( min-width: 1200px ){
|
||||
width:400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,4 +58,4 @@
|
|||
max-width:96%;
|
||||
width: 700px;
|
||||
font-size:1.2em;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
def authenticate_user!
|
||||
unless current_user
|
||||
redirect_to "/enter"
|
||||
respond_to do |format|
|
||||
format.html { redirect_to "/enter" }
|
||||
format.json { render json: { error: "Please sign in" }, status: 401 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -99,7 +102,13 @@ class ApplicationController < ActionController::Base
|
|||
private
|
||||
|
||||
def user_not_authorized
|
||||
flash[:alert] = "You are not authorized to perform this action."
|
||||
redirect_to(request.referrer || root_path)
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
flash[:alert] = "You are not authorized to perform this action."
|
||||
redirect_to(request.referrer || root_path)
|
||||
end
|
||||
|
||||
format.json { render json: { error: "Not authorized" }, status: 401 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,11 +1,50 @@
|
|||
class ChatChannelsController < ApplicationController
|
||||
before_action :authenticate_user!, only: [:moderate]
|
||||
|
||||
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"],
|
||||
message = "The chat channel you are looking for is either invalid or does not exist"
|
||||
render json: { error: message },
|
||||
status: 401
|
||||
end
|
||||
end
|
||||
|
||||
def moderate
|
||||
@chat_channel = ChatChannel.find(params[:id])
|
||||
authorize @chat_channel
|
||||
command = chat_channel_params[:command].split
|
||||
case command[0]
|
||||
when "/ban"
|
||||
banned_user = User.find_by_username(command[1])
|
||||
if banned_user
|
||||
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
|
||||
else
|
||||
render json: { error: "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
|
||||
else
|
||||
render json: { error: "username not found" }, status: 400
|
||||
end
|
||||
when "/clearchannel"
|
||||
@chat_channel.clear_channel
|
||||
else
|
||||
render json: { error: "invalid command" }, status: 400
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def chat_channel_params
|
||||
params.require(:chat_channel).permit(:command)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ class MessagesController < ApplicationController
|
|||
|
||||
def create
|
||||
@message = Message.new(message_params)
|
||||
authorize @message
|
||||
success = false
|
||||
|
||||
if @message.save
|
||||
|
|
@ -29,6 +30,7 @@ class MessagesController < ApplicationController
|
|||
|
||||
def create_pusher_payload(new_message)
|
||||
{
|
||||
user_id: new_message.user.id,
|
||||
username: new_message.user.username,
|
||||
message: new_message.message_markdown,
|
||||
timestamp: new_message.timestamp,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
module.exports = {
|
||||
extends: ['airbnb', 'prettier'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
ecmaVersion: 2017,
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ export function getAllMessages(successCb, failureCb) {
|
|||
fetch('/chat_channels/1', {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}).then(response => response.json())
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
|
@ -16,12 +17,35 @@ export function sendMessage(message, successCb, failureCb) {
|
|||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message_html: message,
|
||||
user_id: window.currentUser.id,
|
||||
chat_channel_id: '1',
|
||||
message: {
|
||||
message_html: message,
|
||||
user_id: window.currentUser.id,
|
||||
chat_channel_id: '1',
|
||||
},
|
||||
}),
|
||||
credentials: 'same-origin',
|
||||
}).then(response => response.json())
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function conductModeration(message, successCb, failureCb) {
|
||||
fetch('/chat_channels/1/moderate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_channel: {
|
||||
command: message,
|
||||
},
|
||||
}),
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
|
|
|||
18
app/javascript/chat/alert.jsx
Normal file
18
app/javascript/chat/alert.jsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Alert = ({ showAlert }) => {
|
||||
const otherClassname = showAlert ? '' : 'chatalert__default--hidden';
|
||||
|
||||
return (
|
||||
<div className={`chatalert__default ${otherClassname}`}>
|
||||
More new messages below
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Alert.propTypes = {
|
||||
showAlert: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default Alert;
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getAllMessages, sendMessage } from './actions';
|
||||
import { scrollToBottom } from './util';
|
||||
import { conductModeration, getAllMessages, sendMessage } from './actions';
|
||||
import { hideMessages, scrollToBottom, setupObserver } from './util';
|
||||
import Alert from './alert';
|
||||
import Compose from './compose';
|
||||
import Message from './message';
|
||||
import setupPusher from './pusher';
|
||||
|
|
@ -9,24 +10,47 @@ 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.handleFailure = this.handleFailure.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.handleSuccess = this.handleSuccess.bind(this);
|
||||
this.observerCallback = this.observerCallback.bind(this);
|
||||
this.receiveAllMessages = this.receiveAllMessages.bind(this);
|
||||
this.receiveNewMessage = this.receiveNewMessage.bind(this);
|
||||
this.clearChannel = this.clearChannel.bind(this);
|
||||
this.redactUserMessages = this.redactUserMessages.bind(this);
|
||||
this.state = {
|
||||
messages: [],
|
||||
scrolled: false,
|
||||
showAlert: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
getAllMessages(this.receiveAllMessages);
|
||||
setupPusher(this.props.pusherKey, this.receiveNewMessage);
|
||||
setupPusher(this.props.pusherKey, {
|
||||
messageCreated: this.receiveNewMessage,
|
||||
channelCleared: this.clearChannel,
|
||||
redactUserMessages: this.redactUserMessages,
|
||||
});
|
||||
setupObserver(this.observerCallback);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
scrollToBottom();
|
||||
if (!this.state.scrolled) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
observerCallback(entries) {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
this.setState({ scrolled: false, showAlert: false });
|
||||
} else {
|
||||
this.setState({ scrolled: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
receiveAllMessages(res) {
|
||||
|
|
@ -36,9 +60,24 @@ class Chat extends Component {
|
|||
receiveNewMessage(message) {
|
||||
const newMessages = this.state.messages.slice();
|
||||
newMessages.push(message);
|
||||
if (newMessages.length > 150) {
|
||||
newMessages.shift();
|
||||
}
|
||||
this.setState({
|
||||
messages: newMessages,
|
||||
showAlert: this.state.scrolled,
|
||||
});
|
||||
}
|
||||
|
||||
redactUserMessages(res) {
|
||||
const newMessages = hideMessages(this.state.messages.slice(), res.userId);
|
||||
this.setState({ messages: newMessages });
|
||||
}
|
||||
|
||||
clearChannel() {
|
||||
this.setState({ messages: [] });
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
|
|
@ -48,16 +87,27 @@ class Chat extends Component {
|
|||
}
|
||||
|
||||
handleMessageSubmit(message) {
|
||||
sendMessage(message, null, this.handleFailure);
|
||||
// should check if user has the priviledge
|
||||
if (message[0] === '/') {
|
||||
conductModeration(message, this.handleSuccess, this.handleFailure);
|
||||
} else {
|
||||
sendMessage(message, this.handleSuccess, this.handleFailure);
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmitOnClick(e) {
|
||||
e.preventDefault();
|
||||
const message = document.getElementById('messageform').value;
|
||||
sendMessage(message, null, this.handleFailure);
|
||||
this.handleMessageSubmit(message);
|
||||
document.getElementById('messageform').value = '';
|
||||
}
|
||||
|
||||
handleSuccess(response) {
|
||||
if (Object.prototype.hasOwnProperty.call(response, 'error')) {
|
||||
console.log(response.error);
|
||||
}
|
||||
}
|
||||
|
||||
handleFailure(err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
|
@ -69,6 +119,7 @@ class Chat extends Component {
|
|||
message={message.message}
|
||||
timeStamp={message.timestamp}
|
||||
color={message.color}
|
||||
hidden={message.hidden}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
|
@ -77,7 +128,11 @@ class Chat extends Component {
|
|||
return (
|
||||
<div className="chatchannel">
|
||||
<div className="chatchannel__messages" id="messagelist">
|
||||
{ this.renderMessage() }
|
||||
{this.renderMessage()}
|
||||
<div id="messagelist__sentinel" />
|
||||
</div>
|
||||
<div className="chatchannel__alerts">
|
||||
<Alert showAlert={this.state.showAlert} />
|
||||
</div>
|
||||
<div className="chatchannel__form">
|
||||
<Compose
|
||||
|
|
|
|||
|
|
@ -6,14 +6,23 @@ import PropTypes from 'prop-types';
|
|||
*/
|
||||
|
||||
const Message = ({
|
||||
user, message, color,
|
||||
user, message, color, hidden,
|
||||
}) => {
|
||||
const spanStyle = { color };
|
||||
const linkStyle = { color: 'inherit' };
|
||||
const messageStyle = { color: hidden ? 'lightgray' : 'inherit' };
|
||||
|
||||
return (
|
||||
<div className="chatmessage">
|
||||
<span className="chatmessage__username" style={spanStyle}>{ user }</span>
|
||||
<span className="chatmessage__username" style={spanStyle}>
|
||||
<a style={linkStyle} href={`/${user}`}>
|
||||
{user}
|
||||
</a>
|
||||
</span>
|
||||
<span className="chatmessage__divider">: </span>
|
||||
<span className="chatmessage__message">{ message } </span>
|
||||
<span className="chatmessage__message" style={messageStyle}>
|
||||
{hidden ? '<message removed>' : message}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -22,6 +31,11 @@ Message.propTypes = {
|
|||
user: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
};
|
||||
|
||||
Message.defaultProps = {
|
||||
hidden: false,
|
||||
};
|
||||
|
||||
export default Message;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import Pusher from 'pusher-js';
|
||||
|
||||
export default function setupPusher(key, callback) {
|
||||
export default function setupPusher(key, callbackObjects) {
|
||||
const pusher = new Pusher(key, {
|
||||
cluster: 'us2',
|
||||
encrypted: true,
|
||||
});
|
||||
|
||||
const channel = pusher.subscribe('1');
|
||||
channel.bind('message-created', callback);
|
||||
channel.bind('message-created', callbackObjects.messageCreated);
|
||||
channel.bind('channel-cleared', callbackObjects.channelCleared);
|
||||
channel.bind('user-banned', callbackObjects.redactUserMessages);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ export function getUserDataAndCsrfToken() {
|
|||
let userData = null;
|
||||
const dataUserAttribute = document.body.getAttribute('data-user');
|
||||
const meta = document.querySelector("meta[name='csrf-token']");
|
||||
if (dataUserAttribute &&
|
||||
if (
|
||||
dataUserAttribute &&
|
||||
dataUserAttribute !== 'undefined' &&
|
||||
dataUserAttribute !== undefined &&
|
||||
meta &&
|
||||
meta.content !== 'undefined' &&
|
||||
meta.content !== undefined) {
|
||||
meta.content !== undefined
|
||||
) {
|
||||
userData = JSON.parse(dataUserAttribute);
|
||||
}
|
||||
i += 1;
|
||||
|
|
@ -30,3 +32,19 @@ export function scrollToBottom() {
|
|||
const element = document.getElementById('messagelist');
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}
|
||||
|
||||
export function setupObserver(callback) {
|
||||
const sentinel = document.querySelector('#messagelist__sentinel');
|
||||
const somethingObserver = new IntersectionObserver(callback);
|
||||
somethingObserver.observe(sentinel);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,11 @@ class ChatChannel < ApplicationRecord
|
|||
has_many :messages
|
||||
|
||||
validates :channel_type, presence: true, inclusion: { in: %w(open) }
|
||||
|
||||
def clear_channel
|
||||
messages.each(&:destroy!)
|
||||
Pusher.trigger(id, "channel-cleared", [].to_json)
|
||||
rescue Pusher::Error => e
|
||||
logger.info "PUSHER ERROR: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -56,4 +56,8 @@ class ApplicationPolicy
|
|||
def user_is_admin?
|
||||
user.has_any_role?(:super_admin, :admin)
|
||||
end
|
||||
|
||||
def user_is_banned?
|
||||
user.has_role?(:banned)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
5
app/policies/chat_channel_policy.rb
Normal file
5
app/policies/chat_channel_policy.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class ChatChannelPolicy < ApplicationPolicy
|
||||
def moderate?
|
||||
!user_is_banned? && user_is_admin?
|
||||
end
|
||||
end
|
||||
11
app/policies/message_policy.rb
Normal file
11
app/policies/message_policy.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class MessagePolicy < ApplicationPolicy
|
||||
def create?
|
||||
!user_is_banned?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_is_banned?
|
||||
user&.has_role?(:banned)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
json.messages @chat_channel.messages do |message|
|
||||
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
|
||||
json.message message.message_markdown
|
||||
json.timestamp message.timestamp
|
||||
|
|
|
|||
|
|
@ -6,43 +6,45 @@
|
|||
<% end %>
|
||||
<% end %>
|
||||
<% title "DEV LIVE" %>
|
||||
<link rel="canonical" href="https://dev.to/live"/>
|
||||
<meta name="description" content="DEV LIVE">
|
||||
<meta name="keywords" content="software development,engineering,rails,javascript,ruby">
|
||||
<link rel="canonical" href="https://dev.to/live"/>
|
||||
<meta name="description" content="DEV LIVE">
|
||||
<meta name="keywords" content="software development,engineering,rails,javascript,ruby">
|
||||
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="https://dev.to/live" />
|
||||
<meta property="og:title" content="DEV LIVE" />
|
||||
<meta property="og:image" content="https://thepracticaldev.s3.amazonaws.com/i/bqzj1pwho9e0jicqo44s.png" />
|
||||
<meta property="og:description" content="DEV Live Events" />
|
||||
<meta property="og:site_name" content="The Practical Dev" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="https://dev.to/live" />
|
||||
<meta property="og:title" content="DEV LIVE" />
|
||||
<meta property="og:image" content="https://thepracticaldev.s3.amazonaws.com/i/bqzj1pwho9e0jicqo44s.png" />
|
||||
<meta property="og:description" content="DEV Live Events" />
|
||||
<meta property="og:site_name" content="The Practical Dev" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@ThePracticalDev">
|
||||
<meta name="twitter:title" content="DEV LIVE">
|
||||
<meta property="og:description" content="DEV Live Events" />
|
||||
<meta name="twitter:image:src" content="https://thepracticaldev.s3.amazonaws.com/i/bqzj1pwho9e0jicqo44s.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@ThePracticalDev">
|
||||
<meta name="twitter:title" content="DEV LIVE">
|
||||
<meta property="og:description" content="DEV Live Events" />
|
||||
<meta name="twitter:image:src" content="https://thepracticaldev.s3.amazonaws.com/i/bqzj1pwho9e0jicqo44s.png">
|
||||
|
||||
<style>
|
||||
<% cache "live-page-css", expires_in: 1.hour do %>
|
||||
<%= Rails.application.assets['live.css'].to_s.html_safe %>
|
||||
<% end %>
|
||||
<% cache "live-page-css", expires_in: 1.hour do %>
|
||||
<%= Rails.application.assets['live.css'].to_s.html_safe %>
|
||||
<% end %>
|
||||
</style>
|
||||
|
||||
<% if current_user&.has_role?(:super_admin) || (Flipflop.live_is_live? && current_user&.workshop_eligible?) %>
|
||||
<div class="live-container">
|
||||
<div class="live-video">
|
||||
<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 class="live-container">
|
||||
<div class="live-component">
|
||||
<div class="live-video">
|
||||
<script src="//player.dacast.com/js/player.js" width="1920" height="1080" id="<%= ENV["DACAST_STREAM_CODE"]%>" player="jw7" jwurl="juQPHW/th8VUHaO/2KkeVlAdGN9ksJZog2z6SH+TyMU=" class="dacast-video"></script>
|
||||
</div>
|
||||
<div class="live-chat-wrapper">
|
||||
<% 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>
|
||||
<div class="live-chat-wrapper">
|
||||
<% 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>
|
||||
<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>
|
||||
<% elsif Flipflop.live_starting_soon? %>
|
||||
<div class="live-upcoming-info">
|
||||
<h1 style="text-align: center"><span style="display: inline-block">DEV LIVE <%= image_tag "emoji/emoji-one-television.png", style: "width: 55px; vertical-align: text-top;" %></span></h1>
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ Rails.application.routes.draw do
|
|||
get "/notifications/:username" => "notifications#index"
|
||||
patch "/onboarding_update" => "users#onboarding_update"
|
||||
get "email_subscriptions/unsubscribe"
|
||||
post "chat_channels/:id/moderate" => "chat_channels#moderate"
|
||||
# resources :users
|
||||
|
||||
### Subscription vanity url
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "ChatChannels", type: :request do
|
||||
let(:user) { create(:user) }
|
||||
let(:test_subject) { create(:user) }
|
||||
let(:chat_channel) { create(:chat_channel) }
|
||||
|
||||
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
|
||||
|
|
@ -30,4 +32,35 @@ RSpec.describe "ChatChannels", type: :request do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /chat_channels/:id/moderate" do
|
||||
it "returns 401 unless user is logged in" do
|
||||
post "/chat_channels/#{chat_channel.id}/moderate",
|
||||
params: { chat_channel: { command: "/ban huh" } },
|
||||
headers: { HTTP_ACCEPT: "application/json" }
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
|
||||
it "returns 401 if user is logged in but not authorized" do
|
||||
sign_in user
|
||||
post "/chat_channels/#{chat_channel.id}/moderate",
|
||||
params: { chat_channel: { command: "/ban huh" } },
|
||||
headers: { HTTP_ACCEPT: "application/json" }
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
|
||||
context "when user is logged-in and authorized" do
|
||||
before do
|
||||
user.add_role :super_admin
|
||||
sign_in user
|
||||
allow(Pusher).to receive(:trigger).and_return(true)
|
||||
end
|
||||
|
||||
it "enforces chat_channel_params" do
|
||||
post "/chat_channels/#{chat_channel.id}/moderate",
|
||||
params: { chat_channel: { command: "/ban #{test_subject.username}" } }
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ RSpec.describe "Editor", type: :request do
|
|||
context "when not logged-in" do
|
||||
it "redirects to /enter" do
|
||||
post "/articles/preview", headers: headers
|
||||
expect(response).to redirect_to("/enter")
|
||||
expect(response).to have_http_status(401)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ RSpec.describe "ImageUploads", type: :request do
|
|||
context "when not logged-in" do
|
||||
it "redirects to /enter" do
|
||||
post "/image_uploads", headers: headers
|
||||
expect(response).to redirect_to("/enter")
|
||||
expect(response).to have_http_status(401)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue