Add content sidecar for chat (#413)
* Rearchitect chat channel subscriptions * Add browser push notifications (attempt) * Add content sidecar for chat * Add live code basics * Add content features to chat * Fix chat_channels test
This commit is contained in:
parent
7bc3e60283
commit
fb20b8adf7
23 changed files with 480 additions and 55 deletions
1
Gemfile
1
Gemfile
|
|
@ -85,6 +85,7 @@ gem "stream_rails", "~> 2.5"
|
|||
gem "stripe", "~> 3.9"
|
||||
gem "therubyracer", "~> 0.12", platforms: :ruby
|
||||
gem "timber", "~> 2.6"
|
||||
gem 'twilio-ruby', '~> 5.10.3'
|
||||
gem "twitter", "~> 6.2"
|
||||
gem "uglifier", "~> 3.2"
|
||||
gem "validate_url", "~> 1.0"
|
||||
|
|
|
|||
|
|
@ -841,6 +841,10 @@ GEM
|
|||
msgpack (~> 1.0)
|
||||
timecop (0.9.1)
|
||||
trollop (2.1.2)
|
||||
twilio-ruby (5.10.3)
|
||||
faraday (~> 0.9)
|
||||
jwt (>= 1.5, <= 2.5)
|
||||
nokogiri (>= 1.6, < 2.0)
|
||||
twitter (6.2.0)
|
||||
addressable (~> 2.3)
|
||||
buftok (~> 0.2.0)
|
||||
|
|
@ -1020,6 +1024,7 @@ DEPENDENCIES
|
|||
therubyracer (~> 0.12)
|
||||
timber (~> 2.6)
|
||||
timecop (~> 0.9)
|
||||
twilio-ruby (~> 5.10.3)
|
||||
twitter (~> 6.2)
|
||||
uglifier (~> 3.2)
|
||||
validate_url (~> 1.0)
|
||||
|
|
|
|||
|
|
@ -79,13 +79,20 @@
|
|||
height: inherit;
|
||||
max-height: inherit;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border: 1px solid $outline-color;
|
||||
box-shadow: $bold-shadow;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.activechatchannel__conversation{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
flex-grow: 3;
|
||||
}
|
||||
|
||||
.activechatchannel__messages {
|
||||
font-size: 15px;
|
||||
padding: 10px 0px;
|
||||
|
|
@ -137,6 +144,36 @@
|
|||
|
||||
}
|
||||
|
||||
.activechatchannel__activecontent{
|
||||
border-left: 2px solid $outline-color;
|
||||
padding: 13px;
|
||||
min-width: 290px;
|
||||
margin-left: -180px;
|
||||
background: white;
|
||||
flex-basis: 80%;
|
||||
z-index: 200;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
overflow-y: scroll;
|
||||
@media screen and ( min-width: 1000px ){
|
||||
margin-left: initial;
|
||||
min-width: 0px;
|
||||
padding: 18px;
|
||||
max-width:480px;
|
||||
}
|
||||
}
|
||||
|
||||
.activechatchannel__activecontentexitbutton{
|
||||
border: 0px;
|
||||
font-size: 36px;
|
||||
color: $medium-gray;
|
||||
padding:0px;
|
||||
background: transparent;
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top:-4px;
|
||||
}
|
||||
|
||||
.chatchannels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -294,3 +331,46 @@
|
|||
.messagelist__sentinel {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.chatcodeeditor{
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 0px;
|
||||
right: 24px;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.CodeMirror{
|
||||
padding: 12px;
|
||||
font-size:0.9em;
|
||||
}
|
||||
|
||||
.cursorelement{
|
||||
border-left: 3px solid $red;
|
||||
animation: blinker 1s linear infinite;
|
||||
padding: 0px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@keyframes blinker {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chat .container {
|
||||
box-shadow: 0px 0px 0px #ffffff !important;
|
||||
border: 0px !important;
|
||||
margin-top: 8px !important;
|
||||
.title{
|
||||
width:95%;
|
||||
font-size:18px;
|
||||
h1{
|
||||
font-size:28px !important;
|
||||
}
|
||||
}
|
||||
.body {
|
||||
font-size:20px;
|
||||
width:96%;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,8 +16,12 @@ module Api
|
|||
end
|
||||
|
||||
def show
|
||||
@article = Article.includes(:user).find(params[:id]).decorate
|
||||
not_found unless @article.published
|
||||
if params[:id] == "by_path"
|
||||
@article = Article.includes(:user).find_by_path(params[:url])&.decorate
|
||||
else
|
||||
@article = Article.includes(:user).find(params[:id])&.decorate
|
||||
end
|
||||
not_found unless @article&.published
|
||||
end
|
||||
|
||||
def onboarding
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ module Api
|
|||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@user = User.find(params[:id])
|
||||
end
|
||||
|
||||
def less_than_one_day_old?(user)
|
||||
range = (Time.now.beginning_of_day - 1.day)..(Time.now)
|
||||
user_identity_age = user.github_created_at ||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@ class ChatChannelsController < ApplicationController
|
|||
def index
|
||||
if params[:state] == "unopened"
|
||||
render_unopened_json_response
|
||||
elsif params[:state] == "additional"
|
||||
render_additional_json_response
|
||||
else
|
||||
render_channels_html
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@chat_channel = ChatChannel.includes(:messages).find_by(id: params[:id])
|
||||
@chat_channel = current_user.chat_channels.includes(:messages).find_by(id: params[:id])
|
||||
if @chat_channel
|
||||
@chat_channel
|
||||
else
|
||||
|
|
@ -71,7 +73,7 @@ class ChatChannelsController < ApplicationController
|
|||
def render_unopened_json_response
|
||||
if current_user.has_role?(:super_admin) || Rails.env.development?
|
||||
@chat_channels_memberships = current_user.
|
||||
chat_channel_memberships.
|
||||
chat_channel_memberships.includes(:chat_channel).
|
||||
where(has_unopened_messages: true).order("updated_at DESC")
|
||||
else
|
||||
@chat_channels_memberships = []
|
||||
|
|
@ -79,19 +81,26 @@ class ChatChannelsController < ApplicationController
|
|||
render "index.json"
|
||||
end
|
||||
|
||||
def render_additional_json_response
|
||||
@chat_channels_memberships = current_user.
|
||||
chat_channel_memberships.includes(:chat_channel).limit(50).order("updated_at DESC")
|
||||
render "index.json"
|
||||
end
|
||||
|
||||
def render_channels_html
|
||||
return unless current_user
|
||||
@chat_channels = current_user.chat_channels.
|
||||
order("last_message_at DESC").
|
||||
includes(:chat_channel_memberships)
|
||||
@chat_channels.each do |channel|
|
||||
channel.current_user = current_user
|
||||
end
|
||||
slug = if params[:slug] && params[:slug].start_with?("@")
|
||||
[current_user.username, params[:slug].gsub("@", "")].sort.join("/")
|
||||
else
|
||||
params[:slug]
|
||||
end
|
||||
@active_channel = ChatChannel.find_by_slug(slug) || @chat_channels.first
|
||||
return unless current_user
|
||||
@chat_channels = current_user.chat_channels.
|
||||
order("last_message_at DESC").
|
||||
limit(25)
|
||||
@chat_channels.each do |channel|
|
||||
channel.current_user = current_user
|
||||
end
|
||||
slug = if params[:slug] && params[:slug].start_with?("@")
|
||||
[current_user.username, params[:slug].gsub("@", "")].sort.join("/")
|
||||
else
|
||||
params[:slug]
|
||||
end
|
||||
@active_channel = ChatChannel.find_by_slug(slug) || @chat_channels.first
|
||||
# @twilio_token = TwilioToken.new(current_user).get
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class PusherController < ApplicationController
|
|||
end
|
||||
|
||||
def valid_presence_channel
|
||||
id = params[:channel_name].split("presence-channel-")[1]
|
||||
id = params[:channel_name].split("presence-channel-")[1].split("-")[0]
|
||||
channel = ChatChannel.find(id)
|
||||
channel.has_member?(current_user)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export function getAllMessages(channelId, successCb, failureCb) {
|
|||
fetch(`/chat_channels/${channelId}`, {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(successCb)
|
||||
|
|
@ -71,6 +72,16 @@ export function conductModeration(
|
|||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function getAdditionalChannels(successCb, failureCb) {
|
||||
fetch(`/chat_channels?state=additional`, {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function sendKeys(subscription, successCb, failureCb) {
|
||||
fetch(`/push_notification_subscriptions`, {
|
||||
|
|
@ -88,4 +99,15 @@ export function sendKeys(subscription, successCb, failureCb) {
|
|||
.then(response => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function getContent(url, successCb, failureCb) {
|
||||
fetch(`${url}`, {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { conductModeration, getAllMessages, sendMessage, sendOpen } from './actions';
|
||||
import { conductModeration, getAllMessages, sendMessage, sendOpen, getAdditionalChannels, getContent } from './actions';
|
||||
import { hideMessages, scrollToBottom, setupObserver, setupNotifications, getNotificationState } from './util';
|
||||
import Alert from './alert';
|
||||
import Channels from './channels';
|
||||
import Compose from './compose';
|
||||
import Message from './message';
|
||||
import Content from './content';
|
||||
import setupPusher from '../src/utils/pusher';
|
||||
|
||||
export default class Chat extends Component {
|
||||
|
|
@ -30,7 +31,8 @@ export default class Chat extends Component {
|
|||
activeChannelId: chatOptions.activeChannelId,
|
||||
showChannelsList: chatOptions.showChannelsList,
|
||||
showTimestamp: chatOptions.showTimestamp,
|
||||
notificationsPermission: null
|
||||
notificationsPermission: null,
|
||||
activeContent: null
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -39,14 +41,13 @@ export default class Chat extends Component {
|
|||
if ( index < 6 ) {
|
||||
this.setupChannel(channel.id);
|
||||
}
|
||||
if (channel.channel_type === "invite_only") {
|
||||
setupPusher(this.props.pusherKey, {
|
||||
channelId: `presence-channel-${channel.id}`,
|
||||
messageCreated: this.receiveNewMessage,
|
||||
channelCleared: this.clearChannel,
|
||||
redactUserMessages: this.redactUserMessages,
|
||||
});
|
||||
}
|
||||
setupPusher(this.props.pusherKey, {
|
||||
channelId: `presence-channel-${channel.id}`,
|
||||
messageCreated: this.receiveNewMessage,
|
||||
channelCleared: this.clearChannel,
|
||||
redactUserMessages: this.redactUserMessages,
|
||||
liveCoding: this.liveCoding
|
||||
});
|
||||
});
|
||||
setupObserver(this.observerCallback);
|
||||
setupPusher(this.props.pusherKey, {
|
||||
|
|
@ -61,6 +62,8 @@ export default class Chat extends Component {
|
|||
null,
|
||||
);
|
||||
this.setState({notificationsPermission: getNotificationState()});
|
||||
getAdditionalChannels(this.loadAdditionalChannels);
|
||||
document.getElementById("messageform").focus();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
|
@ -69,11 +72,23 @@ export default class Chat extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
liveCoding = e => {
|
||||
if (this.state.activeContent === {type_of: "code_editor"}) {
|
||||
return
|
||||
}
|
||||
this.setState({activeContent: {type_of: "code_editor"}})
|
||||
}
|
||||
|
||||
loadAdditionalChannels = channels => {
|
||||
this.setState({chatChannels: channels});
|
||||
}
|
||||
|
||||
setupChannel = channelId => {
|
||||
if (this.state.messages[channelId].length === 0 || this.state.messages[channelId][0].reception_method === 'pushed'){
|
||||
getAllMessages(channelId, this.receiveAllMessages);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
observerCallback = entries => {
|
||||
entries.forEach(entry => {
|
||||
|
|
@ -156,7 +171,9 @@ export default class Chat extends Component {
|
|||
|
||||
handleMessageSubmit = message => {
|
||||
// should check if user has the priviledge
|
||||
if (message[0] === '/') {
|
||||
if (message.startsWith('/code')) {
|
||||
this.setState({activeContent: {type_of: "code_editor"}})
|
||||
} else if (message[0] === '/') {
|
||||
conductModeration(
|
||||
this.state.activeChannelId,
|
||||
message,
|
||||
|
|
@ -180,8 +197,9 @@ export default class Chat extends Component {
|
|||
activeChannelId: parseInt(e.target.dataset.channelId),
|
||||
scrolled: false,
|
||||
showAlert: false,
|
||||
activeContent: null
|
||||
});
|
||||
window.history.replaceState(null, null, "/gether/"+e.target.dataset.channelSlug);
|
||||
window.history.replaceState(null, null, "/connect/"+e.target.dataset.channelSlug);
|
||||
document.getElementById("messageform").focus();
|
||||
if (window.ga && ga.create) {
|
||||
ga('send', 'pageview', location.pathname + location.search);
|
||||
|
|
@ -218,6 +236,26 @@ export default class Chat extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
triggerActiveContent = e => {
|
||||
const target = e.target
|
||||
if (e.target.dataset.content && e.target.dataset.content != "exit") {
|
||||
e.preventDefault();
|
||||
this.setState({activeContent: {type_of: "loading"}})
|
||||
getContent('/api/'+target.dataset.content, this.setActiveContent, null)
|
||||
}
|
||||
else if (target.tagName.toLowerCase() === 'a' && target.href.startsWith('https://dev.to/')) {
|
||||
e.preventDefault();
|
||||
getContent(`/api/articles/by_path?url=${target.href.split('https://dev.to')[1]}`, this.setActiveContent, null)
|
||||
} else if (target.dataset.content === "exit") {
|
||||
e.preventDefault();
|
||||
this.setState({activeContent: null})
|
||||
}
|
||||
}
|
||||
|
||||
setActiveContent = response => {
|
||||
this.setState({activeContent: response});
|
||||
}
|
||||
|
||||
handleChannelOpenSuccess = response => {
|
||||
const newChannelsObj = this.state.chatChannels.map(channel => {
|
||||
if (parseInt(response.channel) === channel["id"]){
|
||||
|
|
@ -232,17 +270,19 @@ export default class Chat extends Component {
|
|||
console.error(err);
|
||||
};
|
||||
|
||||
renderMessage = () => {
|
||||
renderMessages = () => {
|
||||
const { activeChannelId, messages, showTimestamp } = this.state;
|
||||
return messages[activeChannelId].map(message => (
|
||||
<Message
|
||||
user={message.username}
|
||||
userID={message.user_id}
|
||||
profileImageUrl={message.profile_image_url}
|
||||
message={message.message}
|
||||
messageColor={message.messageColor}
|
||||
timestamp={showTimestamp ? message.timestamp : null}
|
||||
color={message.color}
|
||||
type={message.type}
|
||||
onContentTrigger={this.triggerActiveContent}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
|
@ -276,19 +316,27 @@ export default class Chat extends Component {
|
|||
|
||||
renderActiveChatChannel = () => (
|
||||
<div className="activechatchannel">
|
||||
<div className="activechatchannel__messages" id="messagelist">
|
||||
{this.renderMessage()}
|
||||
<div className="messagelist__sentinel" id="messagelist__sentinel" />
|
||||
<div className="activechatchannel__conversation">
|
||||
<div className="activechatchannel__messages" id="messagelist">
|
||||
{this.renderMessages()}
|
||||
<div className="messagelist__sentinel" id="messagelist__sentinel" />
|
||||
</div>
|
||||
<div className="activechatchannel__alerts">
|
||||
<Alert showAlert={this.state.showAlert} />
|
||||
</div>
|
||||
<div className="activechatchannel__form">
|
||||
<Compose
|
||||
handleSubmitOnClick={this.handleSubmitOnClick}
|
||||
handleKeyDown={this.handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="activechatchannel__alerts">
|
||||
<Alert showAlert={this.state.showAlert} />
|
||||
</div>
|
||||
<div className="activechatchannel__form">
|
||||
<Compose
|
||||
handleKeyDown={this.handleKeyDown}
|
||||
handleSubmitOnClick={this.handleSubmitOnClick}
|
||||
<Content
|
||||
resource={this.state.activeContent}
|
||||
onExit={this.triggerActiveContent}
|
||||
activeChannelId={this.state.activeChannelId}
|
||||
pusherKey={this.props.pusherKey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
64
app/javascript/chat/codeEditor.jsx
Normal file
64
app/javascript/chat/codeEditor.jsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import setupPusher from '../src/utils/pusher';
|
||||
|
||||
export default class CodeEditor extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
const editor = document.getElementById("codeeditor");
|
||||
const channel = setupPusher(this.props.pusherKey, {
|
||||
channelId: `presence-channel-${this.props.activeChannelId}`,
|
||||
liveCoding: this.liveCoding,
|
||||
});
|
||||
const myCodeMirror = CodeMirror(editor, {
|
||||
mode: "javascript",
|
||||
theme: "material",
|
||||
autofocus: true,
|
||||
});
|
||||
myCodeMirror.setSize("100%", "100%");
|
||||
//Initial trigger:
|
||||
channel.trigger('client-livecode', {
|
||||
value: myCodeMirror.getValue(),
|
||||
cursorPos: myCodeMirror.getCursor(),
|
||||
});
|
||||
//Coding trigger:
|
||||
myCodeMirror.on('keyup', cm => {
|
||||
channel.trigger('client-livecode', {
|
||||
keyPressed: true,
|
||||
value: cm.getValue(),
|
||||
cursorPos: cm.getCursor(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
liveCoding = e => {
|
||||
console.log("live coding")
|
||||
if (e.keyPressed === true || e.value.length > 0) {
|
||||
let cm = document.querySelector(".CodeMirror").CodeMirror
|
||||
const cursorCoords = e.cursorPos
|
||||
const cursorElement = document.createElement('span');
|
||||
cursorElement.classList.add("cursorelement")
|
||||
cursorElement.style.height = `${(cursorCoords.bottom - cursorCoords.top)}px`;
|
||||
cm.setValue(e.value);
|
||||
cm.setBookmark(e.cursorPos, { widget: cursorElement });
|
||||
} else {
|
||||
let cm = document.querySelector(".CodeMirror").CodeMirror
|
||||
console.log("NEW LIVE CODING")
|
||||
channel.trigger('client-livecode', {
|
||||
value: cm.getValue(),
|
||||
cursorPos: cm.getCursor(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
return <div id="codeeditor" className="chatcodeeditor"></div>
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
74
app/javascript/chat/content.jsx
Normal file
74
app/javascript/chat/content.jsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import CodeEditor from './codeEditor';
|
||||
|
||||
export default class Content extends Component {
|
||||
static propTypes = {
|
||||
resource: PropTypes.object,
|
||||
onExit: PropTypes.func,
|
||||
activeChannelId: PropTypes.number,
|
||||
pusherKey: PropTypes.string,
|
||||
};
|
||||
render() {
|
||||
if (!this.props.resource) {
|
||||
return ""
|
||||
} else {
|
||||
return (
|
||||
<div className="activechatchannel__activecontent">
|
||||
<button
|
||||
class="activechatchannel__activecontentexitbutton"
|
||||
onClick={this.props.onExit}
|
||||
data-content="exit"
|
||||
>×</button>
|
||||
{display(this.props)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function display(props) {
|
||||
if (props.resource.type_of === "loading") {
|
||||
return <div style={{height: "210px",
|
||||
width: "210px",
|
||||
margin:" 15px auto",
|
||||
display: "block",
|
||||
borderRadius: "500px",
|
||||
backgroundColor: "#f5f6f7"}}></div>
|
||||
} else if (props.resource.type_of === "user") {
|
||||
return <div><img
|
||||
src={props.resource.profile_image}
|
||||
style={{height: "210px",
|
||||
width: "210px",
|
||||
margin:" 15px auto",
|
||||
display: "block",
|
||||
borderRadius: "500px"}} />
|
||||
<h1 style={{textAlign: "center"}}>{props.resource.name}</h1>
|
||||
<div style={{fontStyle: "italic"}}>
|
||||
{props.resource.summary}
|
||||
</div>
|
||||
</div>
|
||||
} else if (props.resource.type_of === "article") {
|
||||
console.log(props.resource)
|
||||
return (
|
||||
<div class="container">
|
||||
<div class="title">
|
||||
<h1>{props.resource.title}</h1>
|
||||
<h3>
|
||||
<a href={'/'+props.resource.user.username} class="author">
|
||||
<img class="profile-pic" src={props.resource.user.profile_image_90} alt={props.resource.user.username}/>
|
||||
<span itemprop="name">{props.resource.user.name}</span>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div dangerouslySetInnerHTML={{__html: props.resource.body_html}} ></div>
|
||||
</div>
|
||||
</div>)
|
||||
} else if (props.resource.type_of === "code_editor") {
|
||||
return <CodeEditor
|
||||
activeChannelId={props.activeChannelId}
|
||||
pusherKey={props.pusherKey}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,14 @@ import ErrorMessage from './messages/errorMessage';
|
|||
|
||||
const Message = ({
|
||||
user,
|
||||
userID,
|
||||
message,
|
||||
color,
|
||||
type,
|
||||
messageColor,
|
||||
timestamp,
|
||||
profileImageUrl,
|
||||
onContentTrigger,
|
||||
}) => {
|
||||
const spanStyle = { color };
|
||||
const messageStyle = { color: messageColor };
|
||||
|
|
@ -26,14 +28,22 @@ const Message = ({
|
|||
dangerouslySetInnerHTML={{__html: message}}
|
||||
></span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="chatmessage">
|
||||
<div className="chatmessage__body">
|
||||
<a href={`/${user}`} target="_blank">
|
||||
<div className="chatmessage__body" onClick={onContentTrigger}>
|
||||
<a
|
||||
href={`/${user}`}
|
||||
target="_blank"
|
||||
data-content={`users/${userID}`}
|
||||
onClick={onContentTrigger}
|
||||
>
|
||||
<img
|
||||
className="chatmessagebody__profileimage"
|
||||
src={profileImageUrl}
|
||||
alt={`${user} profile`}
|
||||
data-content={`users/${userID}`}
|
||||
onClick={onContentTrigger}
|
||||
/>
|
||||
</a>
|
||||
<span className="chatmessagebody__username" style={spanStyle}>
|
||||
|
|
@ -41,6 +51,8 @@ const Message = ({
|
|||
className="chatmessagebody__username--link"
|
||||
href={`/${user}`}
|
||||
target="_blank"
|
||||
data-content={`users/${userID}`}
|
||||
onClick={onContentTrigger}
|
||||
>
|
||||
{user}
|
||||
</a>
|
||||
|
|
@ -61,12 +73,14 @@ const Message = ({
|
|||
|
||||
Message.propTypes = {
|
||||
user: PropTypes.string.isRequired,
|
||||
userID: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
messageColor: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
timestamp: PropTypes.string,
|
||||
profileImageUrl: PropTypes.string,
|
||||
onContentTrigger: PropTypes.func,
|
||||
};
|
||||
|
||||
Message.defaultProps = {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class UnopenedChannelNotice extends Component {
|
|||
}
|
||||
|
||||
receiveNewMessage = e => {
|
||||
if (location.pathname.startsWith("/gether") || location.pathname.startsWith("/%F0%9F%92%8C")) {
|
||||
if (location.pathname.startsWith("/connect") || location.pathname.startsWith("/%F0%9F%92%8C")) {
|
||||
return
|
||||
}
|
||||
let channels = this.state.unopenedChannels;
|
||||
|
|
@ -40,7 +40,7 @@ class UnopenedChannelNotice extends Component {
|
|||
if (this.state.visible) {
|
||||
const channels = this.state.unopenedChannels.map(channel => {
|
||||
return <a
|
||||
href={"/gether/"+channel.adjusted_slug}
|
||||
href={"/connect/"+channel.adjusted_slug}
|
||||
style={{
|
||||
background: "#66e2d5",
|
||||
color: "black",
|
||||
|
|
@ -79,7 +79,7 @@ class UnopenedChannelNotice extends Component {
|
|||
}
|
||||
|
||||
export default function getUnopenedChannels(user, successCb) {
|
||||
if (location.pathname.startsWith("/gether") || location.pathname.startsWith("/%F0%9F%92%8C")) {
|
||||
if (location.pathname.startsWith("/connect") || location.pathname.startsWith("/%F0%9F%92%8C")) {
|
||||
return
|
||||
}
|
||||
fetch('/chat_channels?state=unopened', {
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ export default function setupPusher(key, callbackObjects) {
|
|||
cluster: 'us2',
|
||||
encrypted: true,
|
||||
});
|
||||
pusher.unsubscribe(callbackObjects.channelId.toString());
|
||||
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);
|
||||
}
|
||||
channel.bind('client-livecode', callbackObjects.liveCoding);
|
||||
return channel;
|
||||
}
|
||||
|
|
@ -47,6 +47,8 @@ class CacheBuster
|
|||
end
|
||||
bust_home_pages(article)
|
||||
bust_tag_pages(article)
|
||||
bust("/api/articles/#{article.id}")
|
||||
bust("/api/articles/by_path?url=#{article.path}")
|
||||
|
||||
if article.collection
|
||||
article.collection.articles.each do |a|
|
||||
|
|
|
|||
31
app/labor/twilio_token.rb
Normal file
31
app/labor/twilio_token.rb
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
class TwilioToken
|
||||
attr_accessor :user
|
||||
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def get
|
||||
require 'twilio-ruby'
|
||||
|
||||
# Replace with ENV vars
|
||||
account_sid = 'AC1403f2c95cd4912bcb13e1fdc893aef1'
|
||||
api_key = 'SK02d79fd84d84df170adce9a95f826c0d'
|
||||
api_secret = 'MQC9HvLDMbwyQTJNs3x1IHNodidq2Zqf'
|
||||
|
||||
token = Twilio::JWT::AccessToken.new(
|
||||
account_sid,
|
||||
api_key,
|
||||
api_secret,
|
||||
[],
|
||||
identity: user.id
|
||||
)
|
||||
|
||||
grant = Twilio::JWT::AccessToken::VideoGrant.new
|
||||
grant.room = 'DailyStandup'
|
||||
token.add_grant(grant)
|
||||
|
||||
token.to_jwt
|
||||
end
|
||||
|
||||
end
|
||||
11
app/views/api/v0/users/show.json.jbuilder
Normal file
11
app/views/api/v0/users/show.json.jbuilder
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
json.type_of "user"
|
||||
json.username @user.username
|
||||
json.name @user.name
|
||||
json.summary @user.summary
|
||||
json.twitter_username @user.twitter_username
|
||||
json.github_username @user.github_username
|
||||
json.website_url @user.name
|
||||
json.name @user.name
|
||||
json.name @user.name
|
||||
json.name @user.name
|
||||
json.profile_image ProfileImage.new(@user).get(320)
|
||||
|
|
@ -3,6 +3,10 @@
|
|||
<% title "DEV Chat" %>
|
||||
<link rel="canonical" href="https://dev.to/chat"/>
|
||||
<meta name="description" content="DEV Chat">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.38.0/codemirror.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.38.0/codemirror.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.38.0/theme/material.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.38.0/mode/javascript/javascript.min.js"></script>
|
||||
|
||||
<% if user_signed_in? %>
|
||||
<%= javascript_pack_tag 'chat', defer: true %>
|
||||
|
|
@ -25,7 +29,9 @@
|
|||
class="live-chat"
|
||||
data-pusher-key="<%= ENV["PUSHER_KEY"] %>"
|
||||
data-chat-channels="<%= @chat_channels.to_json(methods: :last_opened_at) %>"
|
||||
data-chat-options="<%= {showChannelsList:true, showTimestamp: true, activeChannelId: @active_channel.id }.to_json %>">
|
||||
data-chat-options="<%= {showChannelsList:true, showTimestamp: true, activeChannelId: @active_channel.id }.to_json %>"
|
||||
data-twilio-token="<%= @twilio_token %>"
|
||||
>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= render "devise/registrations/registration_form" %>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
json.array! @chat_channels_memberships do |membership|
|
||||
json.array! (@chat_channels_memberships.sort_by { |m| m.chat_channel.last_message_at}.reverse!) do |membership|
|
||||
json.id membership.chat_channel.id
|
||||
membership.chat_channel.current_user = current_user
|
||||
json.slug membership.chat_channel.slug
|
||||
json.channel_name membership.chat_channel.channel_name
|
||||
json.channel_type membership.chat_channel.channel_type
|
||||
json.last_opened_at membership.chat_channel.last_opened_at
|
||||
json.last_message_at membership.chat_channel.last_message_at
|
||||
json.adjusted_slug membership.chat_channel.adjusted_slug(current_user)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ Rails.application.routes.draw do
|
|||
post "/onboarding", to: "reactions#onboarding"
|
||||
end
|
||||
end
|
||||
resources :users, only: [:index] do
|
||||
resources :users, only: [:index, :show] do
|
||||
collection do
|
||||
get "/sidebar_suggestions", to: "users#sidebar_suggestions"
|
||||
end
|
||||
|
|
@ -101,8 +101,8 @@ Rails.application.routes.draw do
|
|||
get "email_subscriptions/unsubscribe"
|
||||
post "/chat_channels/:id/moderate" => "chat_channels#moderate"
|
||||
post "/chat_channels/:id/open" => "chat_channels#open"
|
||||
get "/gether" => "chat_channels#index"
|
||||
get "/gether/:slug" => "chat_channels#index"
|
||||
get "/connect" => "chat_channels#index"
|
||||
get "/connect/:slug" => "chat_channels#index"
|
||||
|
||||
post "/pusher/auth" => "pusher#auth"
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@
|
|||
"preact": "^8.2.5",
|
||||
"prop-types": "^15.6.0",
|
||||
"pusher-js": "^4.2.2",
|
||||
"sendbird": "^3.0.52"
|
||||
"sendbird": "^3.0.52",
|
||||
"twilio-video": "^1.10.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ RSpec.describe "ChatChannels", type: :request do
|
|||
let(:test_subject) { create(:user) }
|
||||
let(:chat_channel) { create(:chat_channel) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
chat_channel.add_users([user])
|
||||
end
|
||||
|
||||
|
||||
describe "GET /chat_channels/:id" do
|
||||
context "when request is valid" do
|
||||
before do
|
||||
|
|
|
|||
33
yarn.lock
33
yarn.lock
|
|
@ -315,6 +315,14 @@
|
|||
react-split-pane "^0.1.77"
|
||||
react-treebeard "^2.1.0"
|
||||
|
||||
"@twilio/sip.js@^0.7.7":
|
||||
version "0.7.7"
|
||||
resolved "https://registry.yarnpkg.com/@twilio/sip.js/-/sip.js-0.7.7.tgz#cff50f66f9b295d46f219737b72780c7d0e6df52"
|
||||
|
||||
"@twilio/webrtc@github:twilio/twilio-webrtc.js#2.0.1-rc1":
|
||||
version "2.0.1-rc1"
|
||||
resolved "https://codeload.github.com/twilio/twilio-webrtc.js/tar.gz/3532058a5083e69b6ce1ae2984dd59ad5c864f29"
|
||||
|
||||
"@types/node@*":
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.3.0.tgz#3a129cda7c4e5df2409702626892cb4b96546dd5"
|
||||
|
|
@ -718,6 +726,10 @@ async-foreach@^0.1.3:
|
|||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
|
||||
|
||||
async-limiter@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
|
||||
|
||||
async@^1.4.0, async@^1.5.2:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
|
||||
|
|
@ -9424,6 +9436,15 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
|||
version "0.14.5"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
|
||||
twilio-video@^1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/twilio-video/-/twilio-video-1.10.0.tgz#5345f7b664a815a0fc35b99820cdfe19984ae56f"
|
||||
dependencies:
|
||||
"@twilio/sip.js" "^0.7.7"
|
||||
"@twilio/webrtc" twilio/twilio-webrtc.js#2.0.1-rc1
|
||||
ws "^3.3.1"
|
||||
xmlhttprequest "^1.8.0"
|
||||
|
||||
type-check@~0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
|
||||
|
|
@ -9510,6 +9531,10 @@ uid-number@^0.0.6:
|
|||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
|
||||
|
||||
ultron@~1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
|
||||
|
||||
union-value@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
|
||||
|
|
@ -9944,6 +9969,14 @@ write@^0.2.1:
|
|||
dependencies:
|
||||
mkdirp "^0.5.1"
|
||||
|
||||
ws@^3.3.1:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
|
||||
dependencies:
|
||||
async-limiter "~1.0.0"
|
||||
safe-buffer "~5.1.0"
|
||||
ultron "~1.1.0"
|
||||
|
||||
xhr2@0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue