[deploy] Remove old video code and clean up some functionality (#7217)
This commit is contained in:
parent
ea1c0b85af
commit
5ce2152a5c
13 changed files with 220 additions and 532 deletions
|
|
@ -544,6 +544,50 @@
|
|||
}
|
||||
}
|
||||
|
||||
.chat--video-visible {
|
||||
.activechatchannel__conversation {
|
||||
min-width: 25%;
|
||||
@media screen and (min-width: 1440px) {
|
||||
min-width: 45%;
|
||||
}
|
||||
}
|
||||
.activechatchannel__activecontent {
|
||||
&.activechatchannel__activecontent--video {
|
||||
width: 380px;
|
||||
min-width: 380px;
|
||||
@media screen and (max-width: 426px) {
|
||||
width: calc(100% - 16px);
|
||||
min-width: calc(100% - 16px);
|
||||
}
|
||||
iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: 0px;
|
||||
}
|
||||
}
|
||||
width: 300px;
|
||||
min-width: 300px;
|
||||
@media screen and (min-width: 1000px) {
|
||||
width: 400px;
|
||||
min-width: 400px;
|
||||
}
|
||||
@media screen and (min-width: 1300px) {
|
||||
width: 500px;
|
||||
min-width: 500px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat--content-visible .activechatchannel__activecontent.activechatchannel__activecontent--video {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.live-chat-wrapper .activechatchannel__activecontent {
|
||||
min-width: 310px;
|
||||
margin-left: -180px;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ class VideoChatsController < ApplicationController
|
|||
account_sid = ApplicationConfig["TWILIO_ACCOUNT_SID"]
|
||||
api_key = ApplicationConfig["TWILIO_VIDEO_API_KEY"]
|
||||
api_secret = ApplicationConfig["TWILIO_VIDEO_API_SECRET"]
|
||||
@username = "@" + current_user.username
|
||||
@username = display_username
|
||||
@video_type = video_type
|
||||
token = Twilio::JWT::AccessToken.new(
|
||||
account_sid,
|
||||
api_key,
|
||||
|
|
@ -25,4 +26,20 @@ class VideoChatsController < ApplicationController
|
|||
@username = @username
|
||||
@token = token.to_jwt
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def display_username
|
||||
return "@#{params[:username]}" if params[:username] && Rails.env.development? #simpler solo testing in dev
|
||||
|
||||
"@" + current_user.username
|
||||
end
|
||||
|
||||
def video_type
|
||||
if @chat_channel.channel_type == "direct" || @chat_channel.chat_channel_memberships.size < 5
|
||||
"peer-to-peer"
|
||||
else
|
||||
"group"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
exports[`<Chat /> should load chat 1`] = `
|
||||
<div
|
||||
class="chat chat--expanded"
|
||||
class="chat chat--expanded chat--video-not-visible chat--content-not-visible"
|
||||
data-no-instant={true}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Video /> should render properly and test snapshot 1`] = `
|
||||
<div
|
||||
class="chat__videocall"
|
||||
draggable="true"
|
||||
id="chat__videocall"
|
||||
onDrag={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"left": "170.66666666666666px",
|
||||
"top": "76.8px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
class="chat__remotevideoscreen-0"
|
||||
id="videoremotescreen"
|
||||
/>
|
||||
<div
|
||||
class="chat__localvideoscren"
|
||||
id="videolocalscreen"
|
||||
/>
|
||||
<button
|
||||
class="chat__videocallexitbutton"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<button
|
||||
class="chat__videocallcontrolbutton"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
UnMute
|
||||
</button>
|
||||
<button
|
||||
class="chat__videocallcontrolbutton chat__videocallcontrolbutton--videoonoff"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Turn On Video
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { deep } from 'preact-render-spy';
|
||||
import fetch from 'jest-fetch-mock';
|
||||
import Video from '../video';
|
||||
|
||||
global.fetch = fetch;
|
||||
|
||||
let exited;
|
||||
exited = false;
|
||||
|
||||
const exitVideo = () => {
|
||||
exited = true;
|
||||
};
|
||||
|
||||
describe('<Video />', () => {
|
||||
it('should render properly and test snapshot', () => {
|
||||
const tree = render(<Video activeChannelId={12345} onExit={exitVideo} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should have the proper elements, classes and information', () => {
|
||||
const context = deep(<Video activeChannelId={12345} onExit={exitVideo} />);
|
||||
|
||||
// check elements
|
||||
expect(context.find('.chat__videocall').exists()).toEqual(true);
|
||||
expect(context.find('.chat__remotevideoscreen-0').exists()).toEqual(true);
|
||||
expect(context.find('.chat__localvideoscren').exists()).toEqual(true);
|
||||
const exitButton = context.find('.chat__videocallexitbutton');
|
||||
expect(exitButton.exists()).toEqual(true);
|
||||
expect(exitButton.text()).toEqual('×');
|
||||
|
||||
// test exit button behaves
|
||||
exitButton.simulate('click');
|
||||
expect(exited).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -152,17 +152,6 @@ export function getUnopenedChannelIds(successCb) {
|
|||
});
|
||||
}
|
||||
|
||||
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(url, {
|
||||
Accept: 'application/json',
|
||||
|
|
|
|||
|
|
@ -11,16 +11,12 @@ const Channels = ({
|
|||
expanded,
|
||||
filterQuery,
|
||||
channelsLoaded,
|
||||
incomingVideoCallChannelIds,
|
||||
}) => {
|
||||
const channels = chatChannels.map(channel => {
|
||||
const isActive = parseInt(activeChannelId, 10) === channel.chat_channel_id;
|
||||
const isUnopened =
|
||||
!isActive && unopenedChannelIds.includes(channel.chat_channel_id);
|
||||
let newMessagesIndicator = isUnopened ? 'new' : 'old';
|
||||
if (incomingVideoCallChannelIds.indexOf(channel.chat_channel_id) > -1) {
|
||||
newMessagesIndicator = 'video';
|
||||
}
|
||||
const otherClassname = isActive
|
||||
? 'chatchanneltab--active'
|
||||
: 'chatchanneltab--inactive';
|
||||
|
|
@ -122,7 +118,6 @@ Channels.propTypes = {
|
|||
expanded: PropTypes.bool.isRequired,
|
||||
filterQuery: PropTypes.string.isRequired,
|
||||
channelsLoaded: PropTypes.bool.isRequired,
|
||||
incomingVideoCallChannelIds: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
export default Channels;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import Compose from './compose';
|
|||
import Message from './message';
|
||||
import ActionMessage from './actionMessage';
|
||||
import Content from './content';
|
||||
import Video from './video';
|
||||
import VideoContent from './videoContent'
|
||||
|
||||
import setupPusher from '../src/utils/pusher';
|
||||
import debounceAction from '../src/utils/debounceAction';
|
||||
|
|
@ -61,15 +61,12 @@ export default class Chat extends Component {
|
|||
currentUserId: chatOptions.currentUserId,
|
||||
notificationsPermission: null,
|
||||
activeContent: {},
|
||||
videoPath: null,
|
||||
expanded: window.innerWidth > 600,
|
||||
isMobileDevice: typeof window.orientation !== 'undefined',
|
||||
subscribedPusherChannels: [],
|
||||
activeVideoChannelId: null,
|
||||
incomingVideoCallChannelIds: [],
|
||||
videoCallParticipants: [],
|
||||
inviteChannels: [],
|
||||
soundOn: true,
|
||||
videoOn: true,
|
||||
messageOffset: 0,
|
||||
showDeleteModal: false,
|
||||
messageDeleteId: null,
|
||||
|
|
@ -153,32 +150,6 @@ export default class Chat extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
liveCoding = (e) => {
|
||||
const { activeContent, activeChannelId } = this.state;
|
||||
if (activeContent !== { type_of: 'code_editor' }) {
|
||||
activeContent[activeChannelId] = { type_of: 'code_editor' };
|
||||
this.setState({ activeContent });
|
||||
}
|
||||
if (document.querySelector('.CodeMirror')) {
|
||||
const cm = document.querySelector('.CodeMirror').CodeMirror;
|
||||
if (cm && e.context === 'initializing-live-code-channel') {
|
||||
window.pusher.channel(e.channel).trigger('client-livecode', {
|
||||
value: cm.getValue(),
|
||||
cursorPos: cm.getCursor(),
|
||||
});
|
||||
} else if ((cm && e.keyPressed === true) || e.value.length > 0) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
filterForActiveChannel = (channels, id) =>
|
||||
channels.filter(
|
||||
(channel) => channel.chat_channel_id === parseInt(id, 10),
|
||||
|
|
@ -196,9 +167,6 @@ export default class Chat extends Component {
|
|||
channelCleared: this.clearChannel,
|
||||
redactUserMessages: this.redactUserMessages,
|
||||
channelError: this.channelError,
|
||||
liveCoding: this.liveCoding,
|
||||
videoCallInitiated: this.receiveVideoCall,
|
||||
videoCallEnded: this.receiveVideoCallHangup,
|
||||
mentioned: this.mentioned,
|
||||
messageOpened: this.messageOpened,
|
||||
});
|
||||
|
|
@ -479,19 +447,6 @@ export default class Chat extends Component {
|
|||
}));
|
||||
};
|
||||
|
||||
receiveVideoCall = (callObj) => {
|
||||
this.setState((prevState) => ({
|
||||
incomingVideoCallChannelIds: [...prevState, callObj.channelId],
|
||||
}));
|
||||
};
|
||||
|
||||
receiveVideoCallHangup = () => {
|
||||
const { videoCallParticipants } = this.state;
|
||||
if (videoCallParticipants.size < 1) {
|
||||
this.setState({ activeVideoChannelId: null });
|
||||
}
|
||||
};
|
||||
|
||||
redactUserMessages = (res) => {
|
||||
const { messages } = this.state;
|
||||
const newMessages = hideMessages(messages, res.userId);
|
||||
|
|
@ -606,10 +561,7 @@ export default class Chat extends Component {
|
|||
message: '/call',
|
||||
mentionedUsersId: this.getMentionedUsers(message),
|
||||
};
|
||||
this.setActiveContent({
|
||||
path: `/video_chats/${activeChannelId}`,
|
||||
type_of: 'article',
|
||||
});
|
||||
this.setState({videoPath: 'http://localhost:3000/video_chats/' + activeChannelId})
|
||||
sendMessage(messageObject, this.handleSuccess, this.handleFailure);
|
||||
} else if (message.startsWith('/new')) {
|
||||
this.setActiveContentState(activeChannelId, {
|
||||
|
|
@ -651,36 +603,6 @@ export default class Chat extends Component {
|
|||
);
|
||||
};
|
||||
|
||||
answerVideoCall = () => {
|
||||
const { activeChannelId } = this.state;
|
||||
this.setState({
|
||||
activeVideoChannelId: activeChannelId,
|
||||
incomingVideoCallChannelIds: [],
|
||||
});
|
||||
};
|
||||
|
||||
hangupVideoCall = () => {
|
||||
const { activeVideoChannelId } = this.state;
|
||||
window.pusher
|
||||
.channel(`presence-channel-${activeVideoChannelId}`)
|
||||
.trigger('client-endvideocall', {});
|
||||
this.setState({
|
||||
activeVideoChannelId: null,
|
||||
});
|
||||
};
|
||||
|
||||
handleVideoParticipantChange = (participants) => {
|
||||
this.setState({ videoCallParticipants: participants });
|
||||
};
|
||||
|
||||
toggleVideoSound = () => {
|
||||
this.setState((prevState) => ({ soundOn: !prevState.soundOn }));
|
||||
};
|
||||
|
||||
toggleVideoVideo = () => {
|
||||
this.setState((prevState) => ({ videoOn: !prevState.videoOn }));
|
||||
};
|
||||
|
||||
triggerSwitchChannel = (id, slug) => {
|
||||
const {
|
||||
chatChannels,
|
||||
|
|
@ -812,6 +734,8 @@ export default class Chat extends Component {
|
|||
path: `/chat_channel_memberships/${activeChannel.id}/edit`,
|
||||
type_of: 'article',
|
||||
});
|
||||
} else if (content.startsWith('sidecar-video')) {
|
||||
this.setState({videoPath: target.href || target.parentElement.href})
|
||||
} else if (
|
||||
content.startsWith('sidecar') ||
|
||||
content.startsWith('article')
|
||||
|
|
@ -1097,7 +1021,6 @@ export default class Chat extends Component {
|
|||
channelsLoaded={state.channelsLoaded}
|
||||
filterQuery={state.filterQuery}
|
||||
expanded={state.expanded}
|
||||
incomingVideoCallChannelIds={state.incomingVideoCallChannelIds}
|
||||
/>
|
||||
{notificationsState}
|
||||
</div>
|
||||
|
|
@ -1115,7 +1038,6 @@ export default class Chat extends Component {
|
|||
{'>'}
|
||||
</button>
|
||||
<Channels
|
||||
incomingVideoCallChannelIds={state.incomingVideoCallChannelIds}
|
||||
activeChannelId={state.activeChannelId}
|
||||
chatChannels={state.chatChannels}
|
||||
unopenedChannelIds={state.unopenedChannelIds}
|
||||
|
|
@ -1187,7 +1109,7 @@ export default class Chat extends Component {
|
|||
.classList.remove('chatchanneljumpback__hide');
|
||||
};
|
||||
|
||||
renderActiveChatChannel = (channelHeader, incomingCall) => {
|
||||
renderActiveChatChannel = (channelHeader) => {
|
||||
const { state, props } = this;
|
||||
|
||||
return (
|
||||
|
|
@ -1203,7 +1125,6 @@ export default class Chat extends Component {
|
|||
id="messagelist"
|
||||
>
|
||||
{this.renderMessages()}
|
||||
{incomingCall}
|
||||
<div className="messagelist__sentinel" id="messagelist__sentinel" />
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -1252,10 +1173,18 @@ export default class Chat extends Component {
|
|||
pusherKey={props.pusherKey}
|
||||
githubToken={props.githubToken}
|
||||
/>
|
||||
<VideoContent
|
||||
videoPath={state.videoPath}
|
||||
onExit ={this.triggerExitVideo}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
triggerExitVideo = () => {
|
||||
this.setState({videoPath: null})
|
||||
}
|
||||
|
||||
handleMention = (e) => {
|
||||
const { activeChannel } = this.state;
|
||||
const mention = e.keyCode === 64;
|
||||
|
|
@ -1558,53 +1487,17 @@ export default class Chat extends Component {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
let vid = '';
|
||||
let incomingCall = '';
|
||||
if (state.activeVideoChannelId) {
|
||||
vid = (
|
||||
<Video
|
||||
activeChannelId={state.activeChannelId}
|
||||
onToggleSound={this.toggleVideoSound}
|
||||
onToggleVideo={this.toggleVideoVideo}
|
||||
soundOn={state.soundOn}
|
||||
videoOn={state.videoOn}
|
||||
onExit={this.hangupVideoCall}
|
||||
onParticipantChange={this.handleVideoParticipantChange}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
state.incomingVideoCallChannelIds.includes(state.activeChannelId)
|
||||
) {
|
||||
incomingCall = (
|
||||
<div
|
||||
className="activechatchannel__incomingcall"
|
||||
onClick={this.answerVideoCall}
|
||||
onKeyUp={(e) => {
|
||||
if (e.keyCode === 13) this.answerVideoCall();
|
||||
}}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<span role="img" aria-label="waving">
|
||||
👋
|
||||
</span>
|
||||
{' '}
|
||||
Incoming Video Call
|
||||
{' '}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`chat chat--${
|
||||
state.expanded ? 'expanded' : 'contracted'
|
||||
}${detectIOSSafariClass}`}
|
||||
}${detectIOSSafariClass} chat--${ state.videoPath ? 'video-visible' : 'video-not-visible'
|
||||
} chat--${state.activeContent[state.activeChannelId] ? 'content-visible' : 'content-not-visible'}`}
|
||||
data-no-instant
|
||||
>
|
||||
{this.renderChatChannels()}
|
||||
<div className="chat__activechat">
|
||||
{vid}
|
||||
{this.renderActiveChatChannel(channelHeader, incomingCall)}
|
||||
{this.renderActiveChatChannel(channelHeader)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,244 +0,0 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getTwilioToken } from './actions';
|
||||
|
||||
/**
|
||||
* TODO: Instead of calling this function in render, use jsx (<VideoControlButton />).
|
||||
*/
|
||||
function VideoControlButton({ btnClassName, btnClickCallback, btnLabel }) {
|
||||
return (
|
||||
<button type="button" className={btnClassName} onClick={btnClickCallback}>
|
||||
{btnLabel}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
VideoControlButton.propTypes = {
|
||||
btnClassName: PropTypes.string.isRequired,
|
||||
btnClickCallback: PropTypes.func.isRequired,
|
||||
btnLabel: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default class Video extends Component {
|
||||
static propTypes = {
|
||||
activeChannelId: PropTypes.string.isRequired,
|
||||
onToggleSound: PropTypes.func.isRequired,
|
||||
onToggleVideo: PropTypes.func.isRequired,
|
||||
onExit: PropTypes.func.isRequired,
|
||||
soundOn: PropTypes.bool.isRequired,
|
||||
videoOn: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
let leftPx = 40;
|
||||
let topPx = 70;
|
||||
if (window.innerWidth > 1500) {
|
||||
leftPx = window.innerWidth / 5 + 200;
|
||||
topPx = window.innerHeight / 10;
|
||||
} else if (window.innerWidth > 641) {
|
||||
leftPx = window.innerWidth / 6;
|
||||
topPx = window.innerHeight / 10;
|
||||
}
|
||||
this.state = {
|
||||
leftPx,
|
||||
topPx,
|
||||
pageX: null,
|
||||
pageY: null,
|
||||
room: null,
|
||||
participants: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { activeChannelId } = this.props;
|
||||
getTwilioToken(
|
||||
`private-video-channel-${activeChannelId}`,
|
||||
this.setupCallChannel,
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { room } = this.state;
|
||||
room.disconnect();
|
||||
}
|
||||
|
||||
setupCallChannel = response => {
|
||||
const component = this;
|
||||
const { activeChannelId } = this.props;
|
||||
import('twilio-video').then(({ connect, createLocalVideoTrack }) => {
|
||||
connect(
|
||||
response.token,
|
||||
{
|
||||
name: `private-video-channel-${activeChannelId}`,
|
||||
audio: true,
|
||||
type: 'peer-to-peer',
|
||||
video: { width: 640 },
|
||||
},
|
||||
).then(
|
||||
function onConnectSuccess(room) {
|
||||
component.setState({ token: response.token, room });
|
||||
createLocalVideoTrack().then(track => {
|
||||
const localMediaContainer = document.getElementById(
|
||||
'videolocalscreen',
|
||||
);
|
||||
localMediaContainer.appendChild(track.attach());
|
||||
});
|
||||
const roomParticipants = [];
|
||||
room.participants.forEach(participant => {
|
||||
component.triggerRemoteJoin(participant);
|
||||
roomParticipants.push(participant);
|
||||
});
|
||||
component.setState({ participants: roomParticipants });
|
||||
room.on('participantConnected', function onParticipantConnected(
|
||||
participant,
|
||||
) {
|
||||
component.props.onParticipantChange(room.participants);
|
||||
component.triggerRemoteJoin(participant);
|
||||
room.participants.forEach(p => {
|
||||
roomParticipants.push(p);
|
||||
});
|
||||
component.setState({ participants: roomParticipants });
|
||||
room.on(
|
||||
'participantDisconnected',
|
||||
function onParticipantDisconnected() {
|
||||
component.props.onParticipantChange(room.participants);
|
||||
},
|
||||
);
|
||||
participant.on('dominantSpeakerChanged', dominantSpeaker => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
'The new dominant speaker in the Room is:',
|
||||
dominantSpeaker,
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
function onConnectFailure(error) {
|
||||
document.getElementById('videoremotescreen').innerHTML = '';
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Unable to connect to Room: ${error.message}`);
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
triggerRemoteJoin = participant => {
|
||||
participant.on('trackAdded', track => {
|
||||
if (!document.getElementById(`${track.kind}-${track.id}`)) {
|
||||
const trackDiv = document.createElement('div');
|
||||
trackDiv.className = `chat__videocalltrackdiv--${track.kind}`;
|
||||
trackDiv.appendChild(track.attach());
|
||||
document.getElementById('videoremotescreen').appendChild(trackDiv);
|
||||
document.getElementById(
|
||||
'videoremotescreen',
|
||||
).lastChild.id = `${track.kind}-${track.id}`;
|
||||
}
|
||||
});
|
||||
participant.on('trackRemoved', track => {
|
||||
if (document.getElementById(track.id)) {
|
||||
document.getElementById(track.id).outerHTML = '';
|
||||
}
|
||||
});
|
||||
// participant.on('trackDisabled', track => {
|
||||
// console.log('disabled')
|
||||
// console.log('TODO: Show track status on video')
|
||||
// console.log(track.mediaStreamTrack.id)
|
||||
// });
|
||||
// participant.on('trackEnabled', track => {
|
||||
// console.log('enabled')
|
||||
// console.log('TODO: Show track status on video')
|
||||
// console.log(track.mediaStreamTrack.id)
|
||||
// });
|
||||
};
|
||||
|
||||
handleDrag = e => {
|
||||
const { pageX, offsetDiffX, pageY, offsetDiffY } = this.state;
|
||||
if (!pageX) {
|
||||
this.setState({
|
||||
pageX: e.pageX,
|
||||
pageY: e.pageY,
|
||||
offsetDiffX: e.pageX - e.target.offsetLeft,
|
||||
offsetDiffY: e.pageY - e.target.offsetTop,
|
||||
});
|
||||
} else if (e.pageX !== 0) {
|
||||
this.setState({
|
||||
leftPx: pageX + e.pageX - pageX - offsetDiffX,
|
||||
topPx: pageY + e.pageY - pageY - offsetDiffY,
|
||||
});
|
||||
} else if (e.pageX === 0) {
|
||||
this.setState({
|
||||
pageX: null,
|
||||
pageY: null,
|
||||
offsetDiffX: null,
|
||||
offsetDiffY: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
toggleSound = () => {
|
||||
const { room } = this.state;
|
||||
const { onToggleSound } = this.props;
|
||||
if (room) {
|
||||
room.localParticipant.audioTracks.forEach(track => {
|
||||
if (track.isEnabled) {
|
||||
track.disable();
|
||||
} else {
|
||||
track.enable();
|
||||
}
|
||||
});
|
||||
}
|
||||
onToggleSound();
|
||||
};
|
||||
|
||||
toggleVideo = () => {
|
||||
const { room } = this.state;
|
||||
const { onToggleVideo } = this.props;
|
||||
if (room) {
|
||||
room.localParticipant.videoTracks.forEach(track => {
|
||||
if (track.isEnabled) {
|
||||
track.disable();
|
||||
} else {
|
||||
track.enable();
|
||||
}
|
||||
});
|
||||
}
|
||||
onToggleVideo();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { topPx, leftPx, participants } = this.state;
|
||||
const { onExit, soundOn, videoOn } = this.props;
|
||||
return (
|
||||
<div
|
||||
className="chat__videocall"
|
||||
id="chat__videocall"
|
||||
draggable="true"
|
||||
onDrag={this.handleDrag}
|
||||
style={{ left: `${leftPx}px`, top: `${topPx}px` }}
|
||||
>
|
||||
<div
|
||||
id="videoremotescreen"
|
||||
className={`chat__remotevideoscreen-${participants.length}`}
|
||||
/>
|
||||
<div className="chat__localvideoscren" id="videolocalscreen" />
|
||||
{VideoControlButton({
|
||||
btnClassName: 'chat__videocallexitbutton',
|
||||
btnClickCallback: onExit,
|
||||
btnLabel: '×',
|
||||
})}
|
||||
{VideoControlButton({
|
||||
btnClassName: 'chat__videocallcontrolbutton',
|
||||
btnClickCallback: this.toggleSound,
|
||||
btnLabel: soundOn ? 'Mute' : 'UnMute',
|
||||
})}
|
||||
{VideoControlButton({
|
||||
btnClassName:
|
||||
'chat__videocallcontrolbutton chat__videocallcontrolbutton--videoonoff',
|
||||
btnClickCallback: this.toggleVideo,
|
||||
btnLabel: videoOn ? 'Turn Off Video' : 'Turn On Video',
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
25
app/javascript/chat/videoContent.jsx
Normal file
25
app/javascript/chat/videoContent.jsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { h, Component } from 'preact';
|
||||
|
||||
export default class VideoContent extends Component {
|
||||
|
||||
render() {
|
||||
if (!this.props.videoPath) {
|
||||
return ""
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="activechatchannel__activecontent activechatchannel__activecontent--video"
|
||||
id="chat_activecontent_video"
|
||||
>
|
||||
<button
|
||||
className="activechatchannel__activecontentexitbutton"
|
||||
data-content="exit"
|
||||
onClick={this.props.onExit}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<iframe src={this.props.videoPath} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,24 +2,27 @@ const { createLocalVideoTrack, connect } = require('twilio-video');
|
|||
const root = document.getElementById('videochat');
|
||||
const muteButton = document.getElementById('mute-toggle');
|
||||
const videoToggleButton = document.getElementById('videohide-toggle');
|
||||
let numConnected = 0;
|
||||
connect(root.dataset.token, { name: 'room-name', audio: true, type: 'peer-to-peer', video: { width: 640 } }).then(room => {
|
||||
const localMediaContainer = document.getElementById('local-media');
|
||||
const roomType = document.getElementById('room-type').dataset.type//Group
|
||||
|
||||
connect(root.dataset.token, { name: 'room-name', audio: true, type: 'peer-to-peer', video: { width: 800 } }).then(room => {
|
||||
room.participants.forEach(participantConnected);
|
||||
room.on('participantConnected', participantConnected);
|
||||
|
||||
room.on('participantDisconnected', participantDisconnected);
|
||||
room.once('disconnected', error => room.participants.forEach(participantDisconnected));
|
||||
console.log(room.participants.length)
|
||||
muteButton.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
room.localParticipant.audioTracks.forEach(function(trackPub) {
|
||||
if (muteButton.dataset.muted === 'true') {
|
||||
muteButton.dataset.muted = 'false'
|
||||
muteButton.classList.remove('active');
|
||||
muteButton.classList.remove('crayons-btn--danger');
|
||||
muteButton.innerHTML = 'Mute'
|
||||
trackPub.track.enable();
|
||||
} else {
|
||||
muteButton.dataset.muted = 'true'
|
||||
muteButton.classList.add('active');
|
||||
muteButton.classList.add('crayons-btn--danger');
|
||||
muteButton.innerHTML = 'Unmute'
|
||||
trackPub.track.disable();
|
||||
}
|
||||
});
|
||||
|
|
@ -30,11 +33,15 @@ connect(root.dataset.token, { name: 'room-name', audio: true, type: 'peer-to-pee
|
|||
room.localParticipant.videoTracks.forEach(function(trackPub) {
|
||||
if (videoToggleButton.dataset.hidden === 'true') {
|
||||
videoToggleButton.dataset.hidden = 'false'
|
||||
videoToggleButton.classList.remove('active');
|
||||
videoToggleButton.classList.remove('crayons-btn--danger');
|
||||
videoToggleButton.innerHTML = 'Hide';
|
||||
localMediaContainer.classList.remove('video-hidden');
|
||||
trackPub.track.enable();
|
||||
} else {
|
||||
videoToggleButton.dataset.hidden = 'true'
|
||||
videoToggleButton.classList.add('active');
|
||||
videoToggleButton.classList.add('crayons-btn--danger');
|
||||
videoToggleButton.innerHTML = 'Unhide';
|
||||
localMediaContainer.classList.add('video-hidden');
|
||||
trackPub.track.disable();
|
||||
}
|
||||
});
|
||||
|
|
@ -42,9 +49,7 @@ connect(root.dataset.token, { name: 'room-name', audio: true, type: 'peer-to-pee
|
|||
|
||||
});
|
||||
|
||||
|
||||
createLocalVideoTrack().then(track => {
|
||||
const localMediaContainer = document.getElementById('local-media');
|
||||
localMediaContainer.appendChild(track.attach());
|
||||
|
||||
});
|
||||
|
|
@ -54,41 +59,61 @@ function participantConnected(participant) {
|
|||
const div = document.createElement('div');
|
||||
div.id = participant.sid;
|
||||
div.className = "individual-video"
|
||||
div.innerHTML = '<div class="participant-name">'+ participant.identity + '</div>'
|
||||
div.innerHTML = '<div class="participant-info">\
|
||||
<div class="participant-name">'+ participant.identity + '</div>\
|
||||
<div class="disabled-audio-indicator">audio off</div>\
|
||||
<div class="disabled-video-indicator">video off</div>\
|
||||
</div>'
|
||||
|
||||
participant.on('trackSubscribed', track => trackSubscribed(div, track));
|
||||
participant.on('trackUnsubscribed', trackUnsubscribed);
|
||||
|
||||
participant.on('trackDisabled', track => trackDisabled(div, track));
|
||||
participant.on('trackEnabled', track => trackEnabled(div, track));
|
||||
|
||||
participant.tracks.forEach(publication => {
|
||||
if (publication.isSubscribed) {
|
||||
trackSubscribed(div, publication.track);
|
||||
}
|
||||
});
|
||||
root.appendChild(div);
|
||||
numConnected += 1;
|
||||
let gridStyle = 'one-per';
|
||||
if (numConnected > 2) {
|
||||
gridStyle = 'two-per';
|
||||
}
|
||||
if (numConnected > 6) {
|
||||
gridStyle = 'three-per';
|
||||
}
|
||||
if (numConnected > 9) {
|
||||
gridStyle = 'four-per';
|
||||
}
|
||||
root.className = 'video-chat-wrapper video-chat-wrapper-num-' + gridStyle;
|
||||
}
|
||||
|
||||
function participantDisconnected(participant) {
|
||||
console.log('Participant "%s" disconnected', participant.identity);
|
||||
document.getElementById(participant.sid).remove();
|
||||
}
|
||||
|
||||
function trackSubscribed(div, track) {
|
||||
div.appendChild(track.attach());
|
||||
if (!track.isEnabled) {
|
||||
if(track.kind === 'video') {
|
||||
div.classList.add('disabled-video')
|
||||
} else {
|
||||
div.classList.add('disabled-audio')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function trackUnsubscribed(track) {
|
||||
track.detach().forEach(element => element.remove());
|
||||
}
|
||||
|
||||
function trackDisabled(div, track) {
|
||||
if(track.kind === 'video') {
|
||||
div.classList.add('disabled-video')
|
||||
} else {
|
||||
div.classList.add('disabled-audio')
|
||||
}
|
||||
}
|
||||
|
||||
function trackEnabled(div, track) {
|
||||
if(track.kind === 'video') {
|
||||
div.classList.remove('disabled-video')
|
||||
} else {
|
||||
div.classList.remove('disabled-audio')
|
||||
}
|
||||
}
|
||||
|
||||
if (navigator.userAgent === 'DEV-Native-ios') {
|
||||
root.innerHTML = '<h2 class="platform-unavailable-message">This feature is not yet available on iPhone</h2>'
|
||||
}
|
||||
|
|
@ -25,13 +25,7 @@ export default function setupPusher(key, callbackObjects) {
|
|||
channel.bind('message-opened', callbackObjects.messageOpened);
|
||||
channel.bind('channel-cleared', callbackObjects.channelCleared);
|
||||
channel.bind('user-banned', callbackObjects.redactUserMessages);
|
||||
channel.bind('client-livecode', callbackObjects.liveCoding);
|
||||
channel.bind(
|
||||
'client-initiatevideocall',
|
||||
callbackObjects.videoCallInitiated,
|
||||
);
|
||||
|
||||
channel.bind('client-endvideocall', callbackObjects.videoCallEnded);
|
||||
channel.bind('pusher:subscription_error', callbackObjects.channelError);
|
||||
|
||||
return channel;
|
||||
|
|
|
|||
|
|
@ -1,76 +1,109 @@
|
|||
<style>
|
||||
body {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.video-chat-wrapper {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
background: black;
|
||||
background: #171717;
|
||||
overflow: scroll;
|
||||
}
|
||||
video {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.individual-video video {
|
||||
width: 96%;
|
||||
max-width: 800px;
|
||||
margin: 2%;
|
||||
display: inline-block;
|
||||
background: grey;
|
||||
background: #232324;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.video-chat-wrapper-num-two-per .individual-video {
|
||||
width: 50%;
|
||||
.individual-video {
|
||||
overflow: hidden;
|
||||
}
|
||||
.video-chat-wrapper-num-three-per .individual-video {
|
||||
width: 33%;
|
||||
}
|
||||
.video-chat-wrapper-num-four-per .individual-video {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
|
||||
#local-media video {
|
||||
width: 130px;
|
||||
width: 150px;
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
#local-media.video-hidden {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.individual-video {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
.participant-name {
|
||||
background: black;
|
||||
.participant-info {
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
position: absolute;
|
||||
bottom: 4%;
|
||||
left: 3%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
.participant-name {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
background: #171717;
|
||||
padding: 4px 12px;
|
||||
border-radius: 100px;
|
||||
margin: 8px;
|
||||
}
|
||||
.video-controls {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
bottom: 15px;
|
||||
left: 5px;
|
||||
width: calc(100% - 175px);
|
||||
}
|
||||
|
||||
.video-controls button {
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
background: black;
|
||||
border: 2px solid grey;
|
||||
border-radius: 100px;
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
margin: 3px 0px;
|
||||
padding: 3px 0px;
|
||||
}
|
||||
.video-controls button.active {
|
||||
|
||||
.disabled-video video {
|
||||
opacity: 0.1;
|
||||
}
|
||||
.disabled-audio-indicator,.disabled-video-indicator {
|
||||
background: red;
|
||||
color: white;
|
||||
display: none;
|
||||
position: absolute;
|
||||
border-radius: 100px;
|
||||
padding: 4px 10px;
|
||||
right: 5px;
|
||||
bottom: 12px;
|
||||
}
|
||||
.disabled-audio-indicator {
|
||||
right: auto;
|
||||
left: 5px;
|
||||
}
|
||||
.disabled-video .disabled-video-indicator{
|
||||
display: inline-block;
|
||||
}
|
||||
.disabled-audio .disabled-audio-indicator{
|
||||
display: inline-block;
|
||||
}
|
||||
.platform-unavailable-message {
|
||||
padding: 20px;
|
||||
color: white;
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="video-chat-wrapper" id="videochat" data-token="<%= @token %>" data-channel="<%= params[:id] %>" data-user="<%= @username %>">
|
||||
<div id="room-type" data-type="<%= @video_type %>"></div>
|
||||
<div id="remote-media"></div>
|
||||
<div id="local-media"></div>
|
||||
<div class="video-controls">
|
||||
<button id="mute-toggle" data-muted="false">Mute</button>
|
||||
<button id="videohide-toggle" data-hidden="false">Hide Video</button>
|
||||
<button id="mute-toggle" data-muted="false" class="crayons-btn crayons-btn--secondary">Mute</button>
|
||||
<button id="videohide-toggle" data-hidden="false" class="crayons-btn crayons-btn--secondary">Hide</button>
|
||||
</div>
|
||||
<%# TODO Make paramsp[:id] map to a chat channel and ensure the users have permission %>
|
||||
<%# That part is pretty straightforward %>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue