From 372efca385ed2caf90bbe41da84def7ef47dfd64 Mon Sep 17 00:00:00 2001 From: Ben Halpern Date: Mon, 18 Jun 2018 13:06:22 -0400 Subject: [PATCH 1/3] Fix bugs and add experimental video features (#449) --- app/assets/stylesheets/chat.scss | 68 ++++++++++++ app/controllers/chat_channels_controller.rb | 1 - app/controllers/twilio_tokens_controller.rb | 14 +++ app/javascript/chat/actions.js | 11 ++ app/javascript/chat/channels.jsx | 13 ++- app/javascript/chat/chat.jsx | 95 +++++++++++++---- app/javascript/chat/video.jsx | 110 ++++++++++++++++++++ app/javascript/packs/chat.jsx | 7 +- app/javascript/src/utils/pusher.js | 3 + app/labor/twilio_token.rb | 15 ++- app/models/message.rb | 1 - app/views/chat_channels/index.html.erb | 14 ++- config/routes.rb | 1 + 13 files changed, 320 insertions(+), 33 deletions(-) create mode 100644 app/controllers/twilio_tokens_controller.rb create mode 100644 app/javascript/chat/video.jsx diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss index 3c96b4fd3..b12abfe52 100644 --- a/app/assets/stylesheets/chat.scss +++ b/app/assets/stylesheets/chat.scss @@ -137,6 +137,49 @@ width: calc(100% - 45px); } +.chat__videocall{ + height: 480px; + width: 640px; + position:fixed; + background: $black; + border-radius: 12px; + left: 250px; + top: 200px; + cursor: grab; + &:active{ + cursor: grabbing; + } + video{ + border-radius: 12px; + width: 100%; + height: 100%; + } + .chat__localvideoscren{ + height: 120px; + width: 160px; + position: absolute; + right: 15px; + bottom: 15px; + border-radius: 5px; + box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.4); + video{ + border-radius: 5px + } + } +} + +.chat__videocallexitbutton{ + border: 0px; + font-size: 20px; + color: $medium-gray; + padding:1px 6px 2px; + border-radius:6px; + background: rgba(0, 0, 0, 0.1); + position: absolute; + left: 3px; + top: 3px; +} + .activechatchannel { height: inherit; max-height: inherit; @@ -215,6 +258,18 @@ } +.activechatchannel__incomingcall{ + border: 2px solid $green; + box-shadow: 6px 6px 0px $green; + padding: 20px; + margin: 10px; + background: lighten($green, 28%); + font-size: 28px; + font-weight: bold; + cursor:pointer; + animation: pulser 0.5s linear infinite; +} + .activechatchannel__activecontent{ border-left: 2px solid $outline-color; padding: 13px; @@ -321,6 +376,11 @@ } } +.chatchanneltabindicator--phone{ + background: $green; + padding: 0px 8px; +} + .chatchannels__channelslist { padding: 10px 0; flex-grow: 1; @@ -520,6 +580,14 @@ } } +@keyframes pulser { + 50% { + opacity: 0.8; + box-shadow: 2px 2px 0px $green; + padding: 18px; + } +} + .chat .container { box-shadow: 0px 0px 0px #ffffff !important; border: 0px !important; diff --git a/app/controllers/chat_channels_controller.rb b/app/controllers/chat_channels_controller.rb index 967302295..ee36cd46f 100644 --- a/app/controllers/chat_channels_controller.rb +++ b/app/controllers/chat_channels_controller.rb @@ -100,7 +100,6 @@ class ChatChannelsController < ApplicationController @active_channel.current_user = current_user if @active_channel end generate_algolia_search_key - # @twilio_token = TwilioToken.new(current_user).get end def generate_algolia_search_key diff --git a/app/controllers/twilio_tokens_controller.rb b/app/controllers/twilio_tokens_controller.rb new file mode 100644 index 000000000..e440cbc39 --- /dev/null +++ b/app/controllers/twilio_tokens_controller.rb @@ -0,0 +1,14 @@ +class TwilioTokensController < ApplicationController + + def show + video_channel = params[:id] + if (video_channel.start_with?("private-video-channel-") && + ChatChannel.find(video_channel.split("private-video-channel-")[1].to_i)).has_member?(current_user) + @twilio_token = TwilioToken.new(current_user, params[:id]).get + render json: { status: "success", token: @twilio_token }, status: 200 + else + render json: { status: "failure", message: "User does not have permission" }, status: 401 + end + end + +end \ No newline at end of file diff --git a/app/javascript/chat/actions.js b/app/javascript/chat/actions.js index 5d54219b6..8d7e4b05c 100644 --- a/app/javascript/chat/actions.js +++ b/app/javascript/chat/actions.js @@ -109,6 +109,17 @@ export function sendKeys(subscription, successCb, failureCb) { .catch(failureCb); } +export function getTwilioToken(videoChannelName, successCb, failureCb) { + fetch(`/twilio_tokens/${videoChannelName}`, { + Accept: 'application/json', + 'Content-Type': 'application/json', + credentials: 'same-origin', + }) + .then(response => response.json()) + .then(successCb) + .catch(failureCb); +} + export function getContent(url, successCb, failureCb) { fetch(`https://dev.to${url}`, { Accept: 'application/json', diff --git a/app/javascript/chat/channels.jsx b/app/javascript/chat/channels.jsx index c481b3c63..cbda38d8d 100644 --- a/app/javascript/chat/channels.jsx +++ b/app/javascript/chat/channels.jsx @@ -2,7 +2,13 @@ import { h } from 'preact'; import PropTypes from 'prop-types'; import ConfigImage from 'images/three-dots.svg'; -const Channels = ({ activeChannelId, chatChannels, handleSwitchChannel, expanded, filterQuery, channelsLoaded }) => { +const Channels = ({ activeChannelId, + chatChannels, + handleSwitchChannel, + expanded, + filterQuery, + channelsLoaded, + incomingVideoCallChannelIds }) => { const channels = chatChannels.map((channel, index) => { if (!channel) { return} const isActive = parseInt(activeChannelId, 10) === channel.id @@ -58,6 +64,10 @@ const Channels = ({ activeChannelId, chatChannels, handleSwitchChannel, expanded content = name } } + let callIndicator = '' + if (incomingVideoCallChannelIds && incomingVideoCallChannelIds.includes(channel.id)) { + callIndicator = 📞 + } return ( diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx index 1281bc630..bd3668d52 100644 --- a/app/javascript/chat/chat.jsx +++ b/app/javascript/chat/chat.jsx @@ -7,6 +7,8 @@ import Channels from './channels'; import Compose from './compose'; import Message from './message'; import Content from './content'; +import Video from './video'; + import setupPusher from '../src/utils/pusher'; export default class Chat extends Component { @@ -32,10 +34,12 @@ export default class Chat extends Component { showChannelsList: chatOptions.showChannelsList, showTimestamp: chatOptions.showTimestamp, notificationsPermission: null, - activeContent: null, + activeContent: {}, expanded: window.innerWidth > 600, isMobileDevice: typeof window.orientation !== "undefined", - subscribedPusherChannels: [] + subscribedPusherChannels: [], + activeVideoChannelId: null, + incomingVideoCallChannelIds: [] }; } @@ -76,7 +80,9 @@ export default class Chat extends Component { liveCoding = e => { if (this.state.activeContent != {type_of: "code_editor"}) { - this.setState({activeContent: {type_of: "code_editor"}}) + let newActiveContent = this.state.activeContent + newActiveContent[this.state.activeChannelId] = {type_of: "code_editor"} + this.setState({activeContent: newActiveContent}) } if (document.querySelector(".CodeMirror")) { let cm = document.querySelector(".CodeMirror").CodeMirror @@ -112,6 +118,8 @@ export default class Chat extends Component { redactUserMessages: this.redactUserMessages, channelError: this.channelError, liveCoding: this.liveCoding, + videoCallInitiated: this.receiveVideoCall, + videoCallEnded: this.receiveVideoCallHangup }); let subscriptions = this.state.subscribedPusherChannels; subscriptions.push(channelName); @@ -238,6 +246,16 @@ export default class Chat extends Component { }); }; + receiveVideoCall = callObj => { + let incomingCalls = this.state.incomingVideoCallChannelIds; + incomingCalls.push(callObj.channelId) + this.setState({incomingVideoCallChannelIds: incomingCalls}) + } + + receiveVideoCallHangup = () => { + this.setState({activeVideoChannelId: null}) + } + redactUserMessages = res => { // This is shallow clone. This might cause a problem const clonedMessages = Object.assign({}, this.state.messages); @@ -270,7 +288,18 @@ export default class Chat extends Component { handleMessageSubmit = message => { // should check if user has the priviledge if (message.startsWith('/code')) { - this.setState({activeContent: {type_of: "code_editor"}}) + let newActiveContent = this.state.activeContent + newActiveContent[this.state.activeChannelId] = {type_of: "code_editor"} + this.setState({activeContent: newActiveContent}) + } else if (message.startsWith('/call')) { + if (this.state.activeChannel.channel_type === 'direct') { + this.setState({activeVideoChannelId: this.state.activeChannelId}) + window.pusher.channel(`presence-channel-${this.state.activeChannelId}`).trigger('client-initiatevideocall', { + channelId: this.state.activeChannelId + }); + } else { + alert("Calls are only currently available in direct channels"); + } } else if (message[0] === '/') { conductModeration( this.state.activeChannelId, @@ -298,13 +327,26 @@ export default class Chat extends Component { this.triggerSwitchChannel(target.dataset.channelId, target.dataset.channelSlug); }; + answerVideoCall = () => { + this.setState({ + activeVideoChannelId: this.state.activeChannelId, + incomingVideoCallChannelIds: [] + }) + } + + hangupVideoCall = () => { + window.pusher.channel(`presence-channel-${this.state.activeVideoChannelId}`).trigger('client-endvideocall', {}); + this.setState({ + activeVideoChannelId: null, + }) + } + triggerSwitchChannel = (id, slug) => { this.setState({ activeChannel: this.filterForActiveChannel(this.state.chatChannels, id), activeChannelId: parseInt(id), scrolled: false, showAlert: false, - activeContent: null }); this.setupChannel(id); window.history.replaceState(null, null, "/connect/"+slug); @@ -348,23 +390,27 @@ export default class Chat extends Component { triggerActiveContent = e => { const target = e.target + let newActiveContent = this.state.activeContent if (e.target.dataset.content && e.target.dataset.content != "exit") { e.preventDefault(); - this.setState({activeContent: {type_of: "loading-user"}}) + newActiveContent[this.state.activeChannelId] = {type_of: "loading-user"} getContent('/api/'+target.dataset.content, this.setActiveContent, null) - } - else if (target.tagName.toLowerCase() === 'a' && target.href.startsWith('https://dev.to/')) { + } else if (target.tagName.toLowerCase() === 'a' && target.href.startsWith('https://dev.to/')) { e.preventDefault(); - this.setState({activeContent: {type_of: "loading-post"}}) + newActiveContent[this.state.activeChannelId] = {type_of: "loading-post"} 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}) + newActiveContent[this.state.activeChannelId] = null } + this.setState({activeContent: newActiveContent}) } setActiveContent = response => { - this.setState({activeContent: response}); + let newActiveContent = this.state.activeContent + newActiveContent[this.state.activeChannelId] = response + console.log(newActiveContent) + this.setState({activeContent: newActiveContent}); setTimeout(function() { document.getElementById("chat_activecontent").scrollTop = 0; }, 10); @@ -426,17 +472,17 @@ export default class Chat extends Component { let notificationsButton = ""; let notificationsState = ""; if (notificationsPermission === "waiting-permission") { - notificationsButton =
; + notificationsButton =
; } else if (notificationsPermission === "granted") { - notificationsState =
Notificatins On
+ notificationsState =
Notificatins On
} else if (notificationsPermission === "denied") { - notificationsState =
Notificatins Off
+ notificationsState =
Notificatins Off
} if (this.state.expanded) { return (
{notificationsButton} - + {notificationsState}
@@ -459,6 +506,7 @@ export default class Chat extends Component { style={{width: "100%"}} >{">"} ( + renderActiveChatChannel = (channelHeader,incomingCall) => (
{channelHeader}
{this.renderMessages()} + {incomingCall}
@@ -492,7 +541,7 @@ export default class Chat extends Component {
 
let channelHeaderInner = '' const currentChannel = this.state.activeChannel if (currentChannel) { @@ -519,11 +568,19 @@ export default class Chat extends Component { {channelHeaderInner}
} + let vid = '' + let incomingCall = '' + if (this.state.activeVideoChannelId) { + vid =