| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import React, { Component } from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
| import { inject, observer } from "mobx-react"; | ||
| import CONST from '../constants'; | ||
|
|
||
| @inject("store") | ||
| @observer | ||
| export default class ActivePublicName extends Component { | ||
| render() { | ||
| const { history, publicName, disableOptions } = this.props; | ||
|
|
||
| return ( | ||
| <div className="active-public-name"> | ||
| <div className="active-public-name-b"> | ||
| <div className="label">{CONST.UI.LABELS.activePubName}</div> | ||
| <div className="value">{publicName}</div> | ||
| {!disableOptions ? (<div className="opt"> | ||
| <button className="btn" onClick={() => { | ||
| history.push('switch-public-name'); | ||
| }}>{CONST.UI.LABELS.switch}</button> | ||
| </div>) : null} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| ActivePublicName.propTypes = { | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import React, { Component } from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
| import { inject, observer } from "mobx-react"; | ||
|
|
||
| @inject("store") | ||
| @observer | ||
| export default class App extends Component { | ||
| render() { | ||
| return ( | ||
| <div className="root-b"> | ||
| {this.props.children} | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| App.propTypes = { | ||
| children: PropTypes.element.isRequired, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import React, { Component } from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
| import { inject, observer } from "mobx-react"; | ||
|
|
||
| import CONST from '../constants'; | ||
|
|
||
| @inject("store") | ||
| @observer | ||
| export default class Bootstrap extends Component { | ||
| componentDidMount() { | ||
| const { store, history } = this.props; | ||
|
|
||
| store.authorisation() | ||
| .then(() => store.fetchPublicNames()) | ||
| .then(() => store.setupPublicName()) | ||
| .then(() => { | ||
| history.push('home'); | ||
| }) | ||
| } | ||
|
|
||
| getError(err) { | ||
| if(!err) { | ||
| return <span></span>; | ||
| } | ||
|
|
||
| return ( | ||
| <div className="context-b error"> | ||
| <div className="icn"></div> | ||
| <div className="desc">{err}</div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| getProgress(desc) { | ||
| if (!desc) { | ||
| return <span></span>; | ||
| } | ||
|
|
||
| return ( | ||
| <div className="context-b"> | ||
| <div className="icn spinner"></div> | ||
| <div className="desc">{desc}</div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| render() { | ||
| const { store } = this.props; | ||
|
|
||
| let container = undefined; | ||
|
|
||
| if (store.error) { | ||
| container = this.getError(store.error); | ||
| } else { | ||
| container = this.getProgress(store.progress); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="card-1 bootstrap"> | ||
| <div className="logo"> | ||
| <div className="logo-img"></div> | ||
| <div className="logo-desc">{CONST.UI.LABELS.title}</div> | ||
| </div> | ||
| <div className="context"> | ||
| {container} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| Bootstrap.propTypes = { | ||
|
|
||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,349 @@ | ||
| import React, { Component } from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
| import { observable } from 'mobx'; | ||
| import { inject, observer } from "mobx-react"; | ||
| import classNames from 'classnames'; | ||
| import CONST from '../constants'; | ||
| @inject("store") | ||
| @observer | ||
| export default class ChatRoom extends Component { | ||
| @observable originConn = null; | ||
| @observable destConn = null; | ||
|
|
||
| constructor() { | ||
| super(); | ||
| this.friendID = null; | ||
| this.friendUID = null; | ||
| this.offerOptions = CONST.CONFIG.OFFER; | ||
| this.mediaOffer = CONST.CONFIG.MEDIA_OFFER; | ||
| this.originStream = null; | ||
| this.originCandidates = []; | ||
| this.destCandidates = []; | ||
| this.onCreateOfferSuccess = this.onCreateOfferSuccess.bind(this); | ||
| this.onClickCancel = this.onClickCancel.bind(this); | ||
| this.setTimer = this.setTimer.bind(this); | ||
| this.getConnectionStatus = this.getConnectionStatus.bind(this); | ||
| this.timer = null; | ||
| } | ||
|
|
||
| componentWillMount() { | ||
| if (!this.props.store.isAuthorised) { | ||
| return this.props.history.push('/'); | ||
| } | ||
| this.friendID = this.props.match.params.friendId; | ||
| this.friendUID = this.props.match.params.uid; | ||
| this.props.store.initialiseConnInfo(this.friendID, this.friendUID) | ||
| .then(() => { | ||
| this.startStream() | ||
| .then(() => this.setupOrigin()) | ||
| .then(() => this.setupRemote()) | ||
| .then(() => { | ||
| this.originConn.createOffer(this.offerOptions) | ||
| .then(this.onCreateOfferSuccess, (err) => { | ||
| console.error('create offer error :: ', err); | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| componentWillUnmount() { | ||
| this.reset(); | ||
| } | ||
|
|
||
| setTimer(fn) { | ||
| const { store } = this.props; | ||
| this.timer = setTimeout(() => { | ||
| fn.call(store).then((res) => { | ||
| clearTimeout(this.timer); | ||
| if (!res) { | ||
| this.setTimer(fn); | ||
| return; | ||
| } | ||
| if (store.persona === CONST.USER_POSITION.CALLER && store.remoteOffer && store.state === CONST.CONN_STATE.INVITE_ACCEPTED) { | ||
| this.call(); | ||
| } | ||
|
|
||
| if (store.persona === CONST.USER_POSITION.CALLEE && store.remoteAnswer) { | ||
| this.finishConnection(); | ||
| } | ||
| }); | ||
| }, CONST.UI.TIMER_INTERVAL.CONNECTION_POLL); | ||
| } | ||
|
|
||
| startStream() { | ||
| return window.navigator.mediaDevices.getUserMedia(this.mediaOffer) | ||
| .then((stream) => { | ||
| this.originStream = stream; | ||
| this.origin.srcObject = stream; | ||
| }); | ||
| } | ||
|
|
||
| stopAllStreams() { | ||
| if (!this.originStream) { | ||
| return; | ||
| } | ||
| this.originStream.getTracks().forEach((track) => { | ||
| track.stop(); | ||
| }); | ||
| } | ||
|
|
||
| setupOrigin() { | ||
| return new Promise((resolve) => { | ||
| this.originConn = new window.RTCPeerConnection(CONST.CONFIG.SERVER); | ||
| this.originConn.onicecandidate = (e) => { | ||
| if (!e.candidate) { | ||
| this.props.store.setOfferCandidates(this.originCandidates); | ||
| if (!this.friendID) { | ||
| this.props.store.sendInvite() | ||
| .then(() => { | ||
| this.setTimer(this.props.store.checkInviteAccepted); | ||
| }) | ||
| } else { | ||
| this.call(); | ||
| } | ||
| return; | ||
| } | ||
| if (!this.originCandidates) { | ||
| this.originCandidates = []; | ||
| } | ||
| this.originCandidates.push(e.candidate); | ||
| }; | ||
|
|
||
| this.originConn.addStream(this.originStream); | ||
| resolve(); | ||
| }); | ||
| } | ||
|
|
||
| setupRemote() { | ||
| return new Promise((resolve) => { | ||
| this.destConn = new window.RTCPeerConnection(CONST.CONFIG.SERVER); | ||
|
|
||
| this.destConn.onicecandidate = (e) => { | ||
| if (!e.candidate) { | ||
| this.props.store.setAnswerCandidates(this.destCandidates); | ||
| if (!this.friendID) { | ||
| this.props.store.calling().then(() => { | ||
| this.setTimer(this.props.store.checkCallAccepted); | ||
| }); | ||
| } else { | ||
| this.props.store.acceptInvite().then(() => { | ||
| this.setTimer(this.props.store.checkForCalling); | ||
| }); | ||
| } | ||
| return; | ||
| } | ||
| if (!this.destCandidates) { | ||
| this.destCandidates = []; | ||
| } | ||
| this.destCandidates.push(e.candidate); | ||
| }; | ||
|
|
||
| this.destConn.onaddstream = (e) => { | ||
| this.destinaton.srcObject = e.stream; | ||
| } | ||
| resolve(); | ||
| }); | ||
| } | ||
|
|
||
| call() { | ||
| const { store } = this.props; | ||
| this.destConn.setRemoteDescription(store.remoteOffer) | ||
| .then(() => { | ||
| return Promise.all(store.remoteOfferCandidates.map((can) => { | ||
| return this.destConn.addIceCandidate(new RTCIceCandidate(can)) | ||
| .then(() => { | ||
| console.log('set ICE candidate success'); | ||
| }, (err) => { | ||
| console.error('set ICE candidate failed ::', err); | ||
| }); | ||
| })); | ||
| }, (err) => { | ||
| console.error('set destination remote session failed ::', err); | ||
| }) | ||
| .then(() => this.destConn.createAnswer()) | ||
| .then((ansDesc) => { | ||
| this.onCreateAnswerSuccess(ansDesc); | ||
| }, (err) => { | ||
| console.error('create answer error :: ', err); | ||
| }) | ||
| .then(() => { | ||
| if (store.persona === CONST.USER_POSITION.CALLER && store.remoteAnswer) { | ||
| this.finishConnection(); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| onCreateOfferSuccess(offer) { | ||
| this.originConn.setLocalDescription(offer) | ||
| .then(() => { | ||
| console.log('set origin local session success'); | ||
| return this.props.store.setOffer(offer); | ||
| }, (err) => { | ||
| console.error('set origin local session failed ::', err); | ||
| }); | ||
| } | ||
|
|
||
| onCreateAnswerSuccess(answer) { | ||
| const { store } = this.props; | ||
| this.destConn.setLocalDescription(answer) | ||
| .then(() => { | ||
| return store.setAnswer(answer); | ||
| console.log('set destination local session success'); | ||
| }, (err) => { | ||
| console.error('set destination local session failed ::', err); | ||
| }); | ||
| } | ||
|
|
||
| reset() { | ||
| clearTimeout(this.timer); | ||
| this.props.store.resetConnInfo(); | ||
| this.stopAllStreams(); | ||
| } | ||
|
|
||
| endCall(e) { | ||
| e.preventDefault(); | ||
| this.originConn.close(); | ||
| this.destConn.close(); | ||
| this.originConn = null; | ||
| this.destConn = null; | ||
| this.reset(); | ||
| this.props.history.push('/home'); | ||
| } | ||
|
|
||
| onClickCancel(e) { | ||
| e.preventDefault(); | ||
| const self = this; | ||
| const moveHome = () => { | ||
| self.reset(); | ||
| self.props.history.push('/home'); | ||
| }; | ||
| this.props.store.deleteInvite() | ||
| .then(moveHome, moveHome); | ||
| } | ||
|
|
||
| getProgress(progress, error) { | ||
| if (error) { | ||
| return ( | ||
| <div className="progress error"> | ||
| <div className="progress-b"> | ||
| <div className="icn"></div> | ||
| <div className="desc">{error}</div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } else if (progress) { | ||
| return ( | ||
| <div className="progress"> | ||
| <div className="progress-b"> | ||
| <div className="icn spinner"></div> | ||
| <div className="desc">{progress}</div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| return <span></span>; | ||
| } | ||
|
|
||
| getConnectionStatus() { | ||
| let connectionMsg = null; | ||
| const { store } = this.props; | ||
| const { connectionState } = store; | ||
| const { CONN_STATE, UI } = CONST; | ||
| const { CONN_MSGS } = UI; | ||
|
|
||
| const isConnected = (connectionState === CONN_STATE.CONNECTED); | ||
|
|
||
| if (!isConnected) { | ||
| switch (connectionState) { | ||
| case CONN_STATE.INIT: | ||
| connectionMsg = CONN_MSGS.INIT; | ||
| break; | ||
| case CONN_STATE.SEND_INVITE: | ||
| connectionMsg = CONN_MSGS.SEND_INVITE; | ||
| break; | ||
| case CONN_STATE.INVITE_ACCEPTED: | ||
| connectionMsg = CONN_MSGS.INVITE_ACCEPTED; | ||
| break; | ||
| case CONN_STATE.CALLING: | ||
| connectionMsg = CONN_MSGS.CALLING; | ||
| break; | ||
| default: | ||
| connectionMsg = UI.DEFAULT_LOADING_DESC | ||
| } | ||
| } | ||
|
|
||
| const statusClassName = classNames('status', { | ||
| 'connected': connectionState === CONN_STATE.CONNECTED | ||
| }); | ||
|
|
||
| return ( | ||
| <div className={statusClassName}> | ||
| <div className="status-b"> | ||
| <div className="card-1"> | ||
| <div className="logo logo-sm"> | ||
| <div className="logo-img"></div> | ||
| </div> | ||
| <div className="call-for"> | ||
| <div className="call-for-b"> | ||
| <div className="caller">{store.activePublicName}</div> | ||
| <div className="split"></div> | ||
| <div className="callee">{this.friendID || store.friendID}</div> | ||
| </div> | ||
| <div className="id">#{this.friendUID || store.uid}</div> | ||
| </div> | ||
| {this.getProgress(connectionMsg, store.chatRoomError)} | ||
| <div className="opts"> | ||
| <div className="opt"> | ||
| <button className="btn" onClick={this.onClickCancel}>{CONST.UI.LABELS.cancel}</button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| finishConnection() { | ||
| const { store } = this.props; | ||
| this.originConn.setRemoteDescription(store.remoteAnswer) | ||
| .then(() => { | ||
| Promise.all(store.remoteAnswerCandidates.map((can) => { | ||
| return this.originConn.addIceCandidate(new RTCIceCandidate(can)) | ||
| .then(() => { | ||
| console.log('set ICE candidate origin success'); | ||
| }, (err) => { | ||
| console.error('set ICE candidate origin failed ::', err); | ||
| }); | ||
| })).then(() => { | ||
| if (store.persona === CONST.USER_POSITION.CALLER) { | ||
| return; | ||
| } | ||
| store.connected(); | ||
| }); | ||
| }, (err) => { | ||
| console.error('set origin remote session failed ::', err); | ||
| }); | ||
| } | ||
|
|
||
| render() { | ||
| return ( | ||
| <div className="chat-room"> | ||
| <div className="remote"> | ||
| <video ref={(c) => { this.destinaton = c; }} autoPlay></video> | ||
| <div className="origin"> | ||
| <video ref={(c) => { this.origin = c; }} autoPlay></video> | ||
| </div> | ||
| <div className="opts"> | ||
| <div className="opt"> | ||
| <button className="btn end-call" onClick={this.endCall.bind(this)}></button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| {this.getConnectionStatus()} | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| ChatRoom.propTypes = { | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import React, { Component } from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
| import { inject, observer } from "mobx-react"; | ||
|
|
||
| import CONST from '../constants'; | ||
| import ActivePublicName from './active_public_name'; | ||
|
|
||
| @inject("store") | ||
| @observer | ||
| export default class Home extends Component { | ||
| constructor() { | ||
| super(); | ||
| this.timer = null; | ||
| } | ||
|
|
||
| componentWillMount() { | ||
| if (!this.props.store.isAuthorised) { | ||
| return this.props.history.push('/'); | ||
| } | ||
| } | ||
|
|
||
| componentDidMount() { | ||
| this.pollInvite(); | ||
| } | ||
|
|
||
| componentWillUnmount() { | ||
| clearTimeout(this.timer); | ||
| this.timer = null; | ||
| this.props.store.reset(); | ||
| } | ||
|
|
||
| pollInvite() { | ||
| const { store } = this.props; | ||
| const self = this; | ||
| const poll = () => { | ||
| clearTimeout(self.timer); | ||
| self.pollInvite(); | ||
| }; | ||
| this.timer = setTimeout(() => { | ||
| store.fetchInvites(true).then(poll, poll); | ||
| }, CONST.UI.TIMER_INTERVAL.FETCH_INVITES_POLL); | ||
| } | ||
|
|
||
| getActivePublicContainer() { | ||
| const { store, history } = this.props; | ||
| if (!store.activePublicName) { | ||
| return <span></span> | ||
| } | ||
|
|
||
| return <ActivePublicName history={history} publicName={store.activePublicName} /> | ||
| } | ||
|
|
||
| render() { | ||
| const { store, history } = this.props; | ||
|
|
||
| return ( | ||
| <div className="card-1 home"> | ||
| <div className="logo logo-sm"> | ||
| <div className="logo-img"></div> | ||
| </div> | ||
| <div className="split-view"> | ||
| <div className="split-view-b"> | ||
| <div className="split-view-30 split"> | ||
| <div className="invite-label"> | ||
| <div className="invite-label-b"> | ||
| <div className="icn"></div> | ||
| <div className="desc"> | ||
| <a href="#" onClick={e => { | ||
| e.preventDefault(); | ||
| history.push('invites'); | ||
| }}>{`${CONST.UI.LABELS.invites} ${store.invitesCount ? `(${store.invitesCount})` : ''}`}</a> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div className="split-view-70"> | ||
| <button className="btn start-call" onClick={() => { | ||
| history.push('new-chat'); | ||
| }}>{CONST.UI.LABELS.newVideoCall}</button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| {this.getActivePublicContainer()} | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| Home.propTypes = { | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| import React, { Component } from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
| import { inject, observer } from "mobx-react"; | ||
| import classNames from 'classnames'; | ||
| import CONST from '../constants'; | ||
| import ActivePublicName from './active_public_name'; | ||
|
|
||
| @inject("store") | ||
| @observer | ||
| export default class Invites extends Component { | ||
| constructor() { | ||
| super(); | ||
| this.state = { | ||
| selectedInvite: { | ||
| publicId: null, | ||
| uid: null | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| componentWillMount() { | ||
| if (!this.props.store.isAuthorised) { | ||
| return this.props.history.push('/'); | ||
| } | ||
| this.props.store.fetchInvites(); | ||
| } | ||
|
|
||
| componentWillUnmount() { | ||
| this.props.store.reset(); | ||
| } | ||
|
|
||
| onClickInvite(invite) { | ||
| if (!invite.publicId || !invite.uid) { return }; | ||
|
|
||
| this.setState({ selectedInvite: invite }); | ||
| } | ||
|
|
||
| getOptions(onlyCancel) { | ||
| const { store, history } = this.props; | ||
| return ( | ||
| <div className="opts"> | ||
| { | ||
| !onlyCancel ? ( | ||
| <div className="opt"> | ||
| <button | ||
| className="btn primary" | ||
| disabled={!this.state.selectedInvite.publicId || !this.state.selectedInvite.uid || (store.invites.length === 0)} | ||
| onClick={() => { | ||
| history.push(`chat-room/${this.state.selectedInvite.publicId}/${this.state.selectedInvite.uid}`); | ||
| }}>{CONST.UI.LABELS.connect}</button> | ||
| </div> | ||
| ) : null | ||
| } | ||
| <div className="opt"> | ||
| <button className="btn" onClick={() => { | ||
| history.push('/home'); | ||
| }}>{CONST.UI.LABELS.cancel}</button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| getError(msg) { | ||
| return ( | ||
| <div> | ||
| <div className="progress error"> | ||
| <div className="progress-b"> | ||
| <div className="icn"></div> | ||
| <div className="desc">{msg}</div> | ||
| </div> | ||
| </div> | ||
| {this.getOptions(true)} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| getProgressLoader(msg) { | ||
| return ( | ||
| <div className="progress"> | ||
| <div className="progress-b"> | ||
| <div className="icn spinner"></div> | ||
| <div className="desc">{msg}</div> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| // getProgress() { | ||
| // const { store } = this.props; | ||
|
|
||
| // if (store.error) { | ||
| // return ( | ||
| // <div className="progress error"> | ||
| // <div className="progress-b"> | ||
| // <div className="icn"></div> | ||
| // <div className="desc">{store.error}</div> | ||
| // </div> | ||
| // </div> | ||
| // ); | ||
| // } | ||
| // return this.getProgressLoader(store.progress); | ||
| // } | ||
|
|
||
| getInvitesList() { | ||
| const { store, history } = this.props; | ||
| let container = undefined; | ||
| if (store.invites.length === 0) { | ||
| container = <div className="default">{CONST.UI.LABELS.noInvites}</div> | ||
| } else { | ||
| container = ( | ||
| <ul> | ||
| { | ||
| store.invites.map((invite, i) => { | ||
| const listClassName = classNames({ | ||
| active: (invite.uid === this.state.selectedInvite.uid) && (invite.publicId === this.state.selectedInvite.publicId) | ||
| }); | ||
| return ( | ||
| <li key={i} className={listClassName} onClick={() => { | ||
| this.onClickInvite(invite); | ||
| }}>{invite.publicId} {invite.uid}</li> | ||
| ); | ||
| }) | ||
| } | ||
| </ul> | ||
| ); | ||
| } | ||
| return ( | ||
| <div> | ||
| <h3>{CONST.UI.LABELS.chooseInvite}</h3> | ||
| {container} | ||
| {this.getOptions()} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| getActivePublicContainer() { | ||
| const { store, history } = this.props; | ||
| if (!store.activePublicName) { | ||
| return <span></span> | ||
| } | ||
|
|
||
| return <ActivePublicName history={history} publicName={store.activePublicName} disableOptions /> | ||
| } | ||
|
|
||
| render() { | ||
| const { store } = this.props; | ||
| let container = undefined; | ||
|
|
||
| if (store.error) { | ||
| container = this.getError(store.error); | ||
| } else if (store.progress) { | ||
| container = this.getProgressLoader(store.progress); | ||
| } else { | ||
| container = this.getInvitesList(); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="card-1 home"> | ||
| <div className="logo logo-sm"> | ||
| <div className="logo-img"></div> | ||
| </div> | ||
| <div className="list"> | ||
| {container} | ||
| </div> | ||
| {this.getActivePublicContainer()} | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| Invites.propTypes = { | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import React, { Component } from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
| import { inject, observer } from "mobx-react"; | ||
| import CONST from '../constants'; | ||
| import ActivePublicName from './active_public_name'; | ||
| @inject("store") | ||
| @observer | ||
| export default class NewChat extends Component { | ||
| constructor() { | ||
| super(); | ||
| this.onSubmitFriendID = this.onSubmitFriendID.bind(this); | ||
| this.onFocusInput = this.onFocusInput.bind(this); | ||
| } | ||
|
|
||
| componentWillMount() { | ||
| if (!this.props.store.isAuthorised) { | ||
| return this.props.history.push('/'); | ||
| } | ||
| } | ||
|
|
||
| componentDidMount() { | ||
| this.friendID.focus(); | ||
| } | ||
|
|
||
| componentWillUnmount() { | ||
| this.props.store.resetNewChatState(); | ||
| } | ||
|
|
||
| getActivePublicContainer() { | ||
| const { store } = this.props; | ||
| if (!store.activePublicName) { | ||
| return <span></span> | ||
| } | ||
|
|
||
| return <ActivePublicName history={history} publicName={store.activePublicName} disableOptions /> | ||
| } | ||
|
|
||
| getOptions(onlyCancel) { | ||
| const { history } = this.props; | ||
| return ( | ||
| <div className="opts"> | ||
| {!onlyCancel ? (<div className="opt"> | ||
| <button type="submit" className="btn primary-green">{CONST.UI.LABELS.connect}</button> | ||
| </div>) : null} | ||
| <div className="opt"> | ||
| <button type="button" className="btn" onClick={() => { | ||
| history.push('/home'); | ||
| }}>{CONST.UI.LABELS.cancel}</button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| getProgress() { | ||
| const { store } = this.props; | ||
|
|
||
| if (store.newChatError) { | ||
| return ( | ||
| <div className="progress error"> | ||
| <div className="progress-b"> | ||
| <div className="icn"></div> | ||
| <div className="desc">{store.newChatError}</div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| if (store.newChatProgress) { | ||
| return ( | ||
| <div className="progress"> | ||
| <div className="progress-b"> | ||
| <div className="icn spinner"></div> | ||
| <div className="desc">{store.newChatProgress}</div> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| return <span></span> | ||
| } | ||
|
|
||
| onSubmitFriendID(e) { | ||
| e.preventDefault(); | ||
| const { history } = this.props; | ||
| this.props.store.resetNewChatState(); | ||
| const friendID = this.friendID.value.trim(); | ||
| this.props.store.connect(friendID) | ||
| .then(() => { | ||
| history.push('chat-room'); | ||
| }); | ||
| } | ||
|
|
||
| onFocusInput(e) { | ||
| this.props.store.resetNewChatState(); | ||
| } | ||
|
|
||
| render() { | ||
| const { store } = this.props; | ||
| return ( | ||
| <div className="card-1 home"> | ||
| <div className="logo logo-sm"> | ||
| <div className="logo-img"></div> | ||
| </div> | ||
| <div className="new-chat"> | ||
| <h3>{CONST.UI.LABELS.newVideoCall}</h3> | ||
| <div className="new-chat-form"> | ||
| <form onSubmit={this.onSubmitFriendID}> | ||
| <div className="inpt"> | ||
| <input | ||
| type="text" | ||
| name="friendPubId" | ||
| required="required" | ||
| ref={c => { this.friendID = c }} | ||
| onFocus={this.onFocusInput} | ||
| placeholder={CONST.UI.LABELS.friendIdPlaceholder} /> | ||
| </div> | ||
| {!store.newChatProgress ? this.getOptions() : null} | ||
| </form> | ||
| {this.getProgress()} | ||
| </div> | ||
| </div> | ||
| {this.getActivePublicContainer()} | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| NewChat.propTypes = { | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| import React, { Component } from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
| import { inject, observer } from "mobx-react"; | ||
| import classNames from 'classnames'; | ||
|
|
||
| import CONST from '../constants'; | ||
| import ActivePublicName from './active_public_name'; | ||
|
|
||
| @inject("store") | ||
| @observer | ||
| export default class SwitchPublicName extends Component { | ||
| constructor() { | ||
| super(); | ||
| this.state = { | ||
| selectedPubName: null | ||
| }; | ||
| } | ||
| componentWillMount() { | ||
| if (!this.props.store.isAuthorised) { | ||
| return this.props.history.push('/'); | ||
| } | ||
| this.props.store.fetchPublicNames(); | ||
| } | ||
|
|
||
| componentWillUnmount() { | ||
| this.props.store.reset(); | ||
| this.props.store.resetSwitchIDState(); | ||
| } | ||
|
|
||
| onClickPubName(name) { | ||
| if (!name) { return }; | ||
| this.props.store.resetSwitchIDState(); | ||
| this.setState({ selectedPubName: name }); | ||
| } | ||
|
|
||
| getOptions(onlyCancel) { | ||
| const { store, history } = this.props; | ||
|
|
||
| return ( | ||
| <div className="opts"> | ||
| { | ||
| !onlyCancel ? ( | ||
| <div className="opt"> | ||
| <button className="btn primary" disabled={!this.state.selectedPubName} onClick={() => { | ||
| store.activatePublicName(this.state.selectedPubName) | ||
| .then(() => { | ||
| history.push('/home'); | ||
| }); | ||
| }}>{CONST.UI.LABELS.activate}</button> | ||
| </div> | ||
| ) : null | ||
| } | ||
| <div className="opt"> | ||
| <button className="btn" onClick={() => { | ||
| history.push('/home'); | ||
| }}>{CONST.UI.LABELS.cancel}</button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| getProgressLoader(msg) { | ||
| return ( | ||
| <div className="progress"> | ||
| <div className="progress-b"> | ||
| <div className="icn spinner"></div> | ||
| <div className="desc">{msg}</div> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| getError(msg) { | ||
| return ( | ||
| <div> | ||
| <div className="progress error"> | ||
| <div className="progress-b"> | ||
| <div className="icn"></div> | ||
| <div className="desc">{msg}</div> | ||
| </div> | ||
| </div> | ||
| {this.getOptions(true)} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| getProgress() { | ||
| const { store } = this.props; | ||
|
|
||
| if (store.switchIDError) { | ||
| return ( | ||
| <div className="progress error"> | ||
| <div className="progress-b"> | ||
| <div className="icn"></div> | ||
| <div className="desc">{store.switchIDError}</div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| if (store.switchIDProgress) { | ||
| return this.getProgressLoader(store.switchIDProgress); | ||
| } | ||
|
|
||
| return <span></span> | ||
| } | ||
|
|
||
| getPubNamesList() { | ||
| const { store, history } = this.props; | ||
| let container = undefined; | ||
| if (store.publicNames.length === 1) { | ||
| container = <div className="default">{CONST.UI.LABELS.noPublicName}</div> | ||
| } else { | ||
| container = ( | ||
| <ul> | ||
| { | ||
| store.publicNames.map((pub, i) => { | ||
| if (pub === store.activePublicName) { | ||
| return null; | ||
| } | ||
| const listClassName = classNames({ | ||
| active: pub === this.state.selectedPubName | ||
| }); | ||
| return ( | ||
| <li key={i} className={listClassName} onClick={() => { | ||
| this.onClickPubName(pub); | ||
| }}>{pub}</li> | ||
| ); | ||
| }) | ||
| } | ||
| </ul> | ||
| ) | ||
| } | ||
| return ( | ||
| <div> | ||
| <h3>{CONST.UI.LABELS.choosePublicName}</h3> | ||
| {container} | ||
| {!store.switchIDProgress ? this.getOptions() : null} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| getActivePublicContainer() { | ||
| const { store, history } = this.props; | ||
| if (!store.activePublicName) { | ||
| return <span></span> | ||
| } | ||
|
|
||
| return <ActivePublicName history={history} publicName={store.activePublicName} disableOptions /> | ||
| } | ||
|
|
||
| render() { | ||
| const { store } = this.props; | ||
| let container = undefined; | ||
|
|
||
| if (store.error) { | ||
| container = this.getError(store.error); | ||
| } else if (store.progress) { | ||
| container = this.getProgressLoader(store.progress); | ||
| } else { | ||
| container = this.getPubNamesList(); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="card-1 home"> | ||
| <div className="logo logo-sm"> | ||
| <div className="logo-img"></div> | ||
| </div> | ||
| <div className="list"> | ||
| {container} | ||
| </div> | ||
| {this.getProgress()} | ||
| {this.getActivePublicContainer()} | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| SwitchPublicName.propTypes = { | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| export default { | ||
| UI: { | ||
| LABELS: { | ||
| title: 'SAFE WebRTC Signalling', | ||
| activePubName: 'Active Public Name', | ||
| newVideoCall: 'New video call', | ||
| invites: 'Invites', | ||
| switch: 'Switch', | ||
| connect: 'Connect', | ||
| cancel: 'Cancel', | ||
| activate: 'Activate', | ||
| friendIdPlaceholder: 'Enter remote peer\'s Public Name', | ||
| chooseInvite: 'Choose an invite', | ||
| noInvites: 'No invites available', | ||
| noPublicName: 'No Public Name available to switch', | ||
| choosePublicName: 'Select Public Name', | ||
| }, | ||
| MESSAGES: { | ||
| authorise: 'Authorising with Authenticator', | ||
| authoriseFail: 'Authorisation failed', | ||
| initialise: 'Initialising application', | ||
| initialiseFail: 'Failed to initialise application', | ||
| noPubNameFound: 'No Public Name found.', | ||
| fetchPublicName: 'Fetching Public Names', | ||
| fetchPublicNameFail: 'Unable to fetch Public Names', | ||
| fetchInvites: 'Fetching invites', | ||
| fetchInvitesFail: 'Unable to fetch invites', | ||
| activatePublicName: 'Activating Public Name', | ||
| activatePublicNameFail: 'Failed to activate Public Name', | ||
| connecting: 'Connecting with remote peer', | ||
| connectingFail: 'Failed to connect with remote peer', | ||
| invalidPublicName: 'Invalid Public Name', | ||
| cantInviteYourself: 'Can\'t invite yourself', | ||
| inviteAcceptFail: 'Failed to accept invite', | ||
| callAcceptFail: 'Failed after remote peer accepted the call', | ||
| checkCallingFail: 'Failed to accept remote peer call', | ||
| initialisationFail: 'Failed to initialise the connection', | ||
| sendInviteFail: 'Failed to send invitation to remote peer', | ||
| callingFail: 'Failed to call remote peer', | ||
| connectingFail: 'Failed to connect with remote peer' | ||
| }, | ||
| DEFAULT_LOADING_DESC: 'Please wait...', | ||
| CONN_MSGS: { | ||
| INIT: 'Initialising connection', | ||
| SEND_INVITE: 'Invite sent. Waiting for the remote peer to accept the connection', | ||
| INVITE_ACCEPTED: 'Invite accepted. Establishing connection with remote peer', | ||
| CALLING: 'Remote peer accepted invite. Establishing connection', | ||
| }, | ||
| CONN_TIMER_INTERVAL: 2000, | ||
| TIMER_INTERVAL: { | ||
| FETCH_INVITES_POLL: 5000, | ||
| CONNECTION_POLL: 4000, | ||
| }, | ||
| }, | ||
| CONFIG: { | ||
| SERVER: { | ||
| iceServers: [ | ||
| { url: 'STUN_SERVER_URL' }, // fill STUN Server url | ||
| { | ||
| url: 'TURN_SERVER_URL', // fill turn server url | ||
| credential: 'TURN_PASSWORD', // fill turn server password | ||
| username: 'TURN_USERNAME' // fill turn server username | ||
| }, | ||
| ] | ||
| }, | ||
| OFFER: { | ||
| offerToReceiveAudio: 1, | ||
| offerToReceiveVideo: 1 | ||
| }, | ||
| MEDIA_OFFER: { | ||
| audio: true, | ||
| video: true | ||
| }, | ||
| }, | ||
| USER_POSITION: { | ||
| CALLER: 'CALLER', | ||
| CALLEE: 'CALLEE', | ||
| }, | ||
| CONN_STATE: { | ||
| INIT: 'INIT', | ||
| SEND_INVITE: 'SEND_INVITE', | ||
| INVITE_ACCEPTED: 'INVITE_ACCEPTED', | ||
| CALLING: 'CALLING', | ||
| CONNECTED: 'CONNECTED', | ||
| }, | ||
| NET_STATE: { | ||
| INIT: 'Init', | ||
| DISCONNECTED: 'Disconnected', | ||
| CONNECTED: 'Connected', | ||
| UNKNOWN: 'Unknown', | ||
| }, | ||
| PERMISSIONS: { | ||
| READ: 'Read', | ||
| INSERT: 'Insert', | ||
| UPDATE: 'Update', | ||
| }, | ||
| MD_KEY: '@webrtcSignalSample', | ||
| SELECTED_PUB_NAME_KEY: 'selected_pub_name', | ||
| MD_META_KEY: '_metadata', | ||
| TYPE_TAG: { | ||
| CHANNEL: 15005, | ||
| DNS: 15001, | ||
| }, | ||
| CRYPTO_KEYS: { | ||
| SEC_SIGN_KEY: '__SEC_SIGN_KEY__', | ||
| PUB_SIGN_KEY: '__PUB_SIGN_KEY__', | ||
| SEC_ENC_KEY: '__SEC_ENC_KEY__', | ||
| PUB_ENC_KEY: '__PUB_ENC_KEY__', | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <html> | ||
| <head> | ||
| <title>SAFE WebRTC example</title> | ||
| </head> | ||
| <body> | ||
| <div id="__WEBRTC_APP__" class="root"></div> | ||
| <script defer src="/static/bundle.js"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import React from 'react'; | ||
| import {render} from 'react-dom'; | ||
| import createHashHistory from 'history/createHashHistory'; | ||
| import { Provider } from 'mobx-react'; | ||
| import { RouterStore, syncHistoryWithStore } from 'mobx-react-router'; | ||
| import { Router } from 'react-router'; | ||
| import AppRouter from './router'; | ||
| import AppStore from './app_store'; | ||
|
|
||
| const browserHistory = createHashHistory(); | ||
| const routingStore = new RouterStore(); | ||
| const appStore = new AppStore(); | ||
|
|
||
|
|
||
| const stores = { | ||
| routing: routingStore, | ||
| store: appStore, | ||
| }; | ||
|
|
||
| const history = syncHistoryWithStore(browserHistory, routingStore); | ||
|
|
||
| render( | ||
| <Provider {...stores}> | ||
| <Router history={history}> | ||
| <AppRouter /> | ||
| </Router> | ||
| </Provider>, | ||
| document.getElementById('__WEBRTC_APP__') | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import React from 'react'; | ||
| import { Switch, Route } from 'react-router'; | ||
| import App from './components/app'; | ||
| import Bootstrap from './components/bootstrap'; | ||
| import Home from './components/home'; | ||
| import SwitchPublicName from './components/switch_public_name'; | ||
| import Invites from './components/invites'; | ||
| import NewChat from './components/new_chat'; | ||
| import ChatRoom from './components/chat_room'; | ||
|
|
||
| export default () => ( | ||
| <App> | ||
| <Switch> | ||
| <Route path="/home" component={Home} /> | ||
| <Route path="/switch-public-name" component={SwitchPublicName} /> | ||
| <Route path="/invites" component={Invites} /> | ||
| <Route path="/new-chat" component={NewChat} /> | ||
| <Route path="/chat-room/:friendId?/:uid?" component={ChatRoom} /> | ||
| <Route path="/" component={Bootstrap} /> | ||
| </Switch> | ||
| </App> | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| .btn | ||
| +btnDefault() | ||
| font-size: 14px | ||
| line-height: 30px | ||
| padding: 0 16px | ||
| font-weight: 600 | ||
| border-radius: 3px | ||
| color: $color-smoke-medium | ||
| &:hover | ||
| background-color: $color-smoke-light | ||
| color: $color-snow-light | ||
| &:disabled | ||
| background-color: transparent !important | ||
| color: $color-smoke-light !important | ||
| &.start-call | ||
| font-size: 24px | ||
| background-color: $color-green | ||
| color: #ffffff | ||
| line-height: 70px | ||
| padding: 0 48px | ||
| font-weight: normal | ||
| &:hover | ||
| background-color: $color-green-dark | ||
| &.primary | ||
| color: $color-pr-blue | ||
| &:hover | ||
| background-color: $color-pr-blue | ||
| color: #fff | ||
| &.danger | ||
| color: $color-error | ||
| &:hover | ||
| background-color: $color-snow-light | ||
| color: $color-error | ||
| &.end-call | ||
| width: 50px | ||
| height: 50px | ||
| background-color: $color-end-call | ||
| background-image: url($end-call-url) | ||
| background-position: center | ||
| background-size: 24px | ||
| background-repeat: no-repeat | ||
| border-radius: 50% | ||
| transform: rotate(135deg) | ||
| &:hover | ||
| background-color: $color-end-call-hover |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| .chat-room | ||
| position: fixed | ||
| width: 100% | ||
| height: 100% | ||
| background-color: $color-smoke-dark | ||
| .remote | ||
| width: $remote-width | ||
| height: $remote-width | ||
| background: $color-smoke-medium | ||
| position: relative | ||
| top: 130px | ||
| left: 50% | ||
| transform: translate(-50%, 0) | ||
| border-radius: 5px | ||
| overflow: hidden | ||
| video | ||
| width: 100% | ||
| height: 100% | ||
| transform: scale(1.3) | ||
| .origin | ||
| width: $origin-width | ||
| height: $origin-width | ||
| background: $color-smoke-light | ||
| position: absolute | ||
| top: 0 | ||
| right: 0 | ||
| border-radius: 5px | ||
| overflow: hidden | ||
| video | ||
| width: 100% | ||
| height: 100% | ||
| transform: scale(1.3) | ||
| .opts | ||
| width: 100% | ||
| position: absolute | ||
| left: 0 | ||
| bottom: 24px | ||
| text-align: center | ||
| .opts | ||
| display: inline-block | ||
| .status | ||
| text-align: center | ||
| .status-b | ||
| width: 100% | ||
| height: 100% | ||
| background-color: rgba(0, 0, 0, 0.5) | ||
| position: absolute | ||
| top: 0 | ||
| left: 0 | ||
| .card-1 | ||
| min-height: 220px | ||
| .call-for | ||
| text-align: center | ||
| width: 100% | ||
| position: relative | ||
| .call-for-b | ||
| display: inline-block | ||
| .caller, | ||
| .callee | ||
| font-size: 18px | ||
| color: $color-smoke-medium | ||
| line-height: 50px | ||
| width: 50% | ||
| .caller | ||
| float: left | ||
| padding-right: 50px | ||
| text-align: right | ||
| .callee | ||
| float: left | ||
| padding-left: 50px | ||
| text-align: left | ||
| .split | ||
| float: left | ||
| width: 100px | ||
| height: 30px | ||
| margin: 10px 0 | ||
| background-image: url($connect-url) | ||
| background-position: center | ||
| background-size: contain | ||
| background-repeat: no-repeat | ||
| position: absolute | ||
| left: 50% | ||
| transform: translate(-50%, 0) | ||
| &.connected | ||
| .status-b | ||
| height: auto | ||
| background-color: transparent | ||
| .card-1 | ||
| top: 16px | ||
| transform: translate(-50%, 0) | ||
| min-height: 80px | ||
| padding: 16px | ||
| .logo | ||
| width: $logo-xs | ||
| height: $logo-xs | ||
| top: 16px | ||
| left: 16px | ||
| &.logo-sm | ||
| .logo-img | ||
| height: 30px | ||
| .call-for | ||
| .caller, | ||
| .callee | ||
| line-height: 30px | ||
| .split | ||
| margin: 0 | ||
| .opts | ||
| display: none !important | ||
| .id | ||
| font-size: 14px | ||
| color: $color-smoke-light | ||
| font-weight: 600 | ||
| width: 100% | ||
| margin-top: 8px | ||
| line-height: 24px | ||
| .opts | ||
| display: inline-block | ||
| .opt | ||
| margin: 16px 0 0 | ||
| button.primary-green | ||
| font-size: 18px | ||
| background-color: $color-green | ||
| color: #ffffff | ||
| line-height: 46px | ||
| padding: 0 36px | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| * | ||
| margin: 0 | ||
| padding: 0 | ||
| box-sizing: border-box | ||
|
|
||
| body | ||
| font-family: 'Open Sans', sans-serif | ||
| background-color: $body-bg | ||
|
|
||
| .card-1 | ||
| width: $card-width | ||
| min-height: $card-height | ||
| background-color: #ffffff | ||
| border-radius: 5px | ||
| transform: translate(-50%, -50%) | ||
| position: absolute | ||
| top: 50% | ||
| left: 50% | ||
| padding: $card-padd | ||
|
|
||
| .default | ||
| font-size: 16px | ||
| color: $color-smoke-medium | ||
| text-align: center | ||
| line-height: 36px | ||
| padding: 8px 0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,247 @@ | ||
| .logo | ||
| text-align: center | ||
| .logo-img | ||
| display: block | ||
| height: $logo-lg | ||
| background-image: url($logo-url) | ||
| background-position: center | ||
| background-size: contain | ||
| background-repeat: no-repeat | ||
| .logo-desc | ||
| font-size: 14px | ||
| font-weight: normal | ||
| color: $color-logo-desc | ||
| transform: translateY(-10px) | ||
| &.logo-sm | ||
| width: $logo-sm | ||
| position: absolute | ||
| top: $card-padd | ||
| left: $card-padd | ||
| .logo-img | ||
| height: $logo-sm | ||
|
|
||
| .bootstrap | ||
| padding: 24px | ||
| .logo | ||
| margin-top: 20px | ||
| .context | ||
| margin-top: 20px | ||
| text-align: center | ||
| .context-b | ||
| display: inline-block | ||
| .icn | ||
| display: inline-block | ||
| width: 30px | ||
| height: 30px | ||
| margin-right: 16px | ||
| float: left | ||
| .desc | ||
| display: inline-block | ||
| line-height: 30px | ||
| float: left | ||
| font-size: 18px | ||
| font-weight: normal | ||
| color: $color-smoke-medium | ||
| &.error | ||
| .icn | ||
| background-image: url($error-url) | ||
| .desc | ||
| color: $color-error | ||
|
|
||
| .home | ||
| min-height: 500px | ||
| .split-view | ||
| width: 100% | ||
| margin-top: 10px | ||
| padding: 24px | ||
| display: block | ||
| position: absolute | ||
| top: 50% | ||
| left: 50% | ||
| transform: translate(-50%, -50%) | ||
| .split-view-b | ||
| width: 100% | ||
| display: table | ||
| min-height: 130px | ||
| .split-view-30, | ||
| .split-view-70 | ||
| display: table-cell | ||
| vertical-align: middle | ||
| text-align: center | ||
| position: relative | ||
| .split-view-30 | ||
| width: 30% | ||
| &.split | ||
| &:after | ||
| content: "" | ||
| position: absolute | ||
| width: 2px | ||
| height: 80% | ||
| background-color: $color-smoke-light | ||
| top: 10% | ||
| right: -2px | ||
| z-index: 9 | ||
| .split-view-70 | ||
| width: 70% | ||
|
|
||
| .invite-label | ||
| .invite-label-b | ||
| display: inline-block | ||
| .icn | ||
| width: 24px | ||
| height: 24px | ||
| background-image: url($invite-url) | ||
| background-position: center | ||
| background-size: contain | ||
| background-repeat: no-repeat | ||
| float: left | ||
| margin-right: 16px | ||
| .desc | ||
| font-size: 18px | ||
| line-height: 24px | ||
| float: left | ||
| color: $color-smoke-medium | ||
| a | ||
| text-decoration: none | ||
| color: inherit | ||
| &:hover | ||
| .desc | ||
| a | ||
| color: $color-smoke-dark | ||
|
|
||
| .active-public-name | ||
| text-align: center | ||
| width: 100% | ||
| position: absolute | ||
| bottom: $card-padd + 5px | ||
| left: 0 | ||
| .active-public-name-b | ||
| display: inline-block | ||
| .label | ||
| float: left | ||
| line-height: 30px | ||
| margin-right: 16px | ||
| font-size: 14px | ||
| font-weight: normal | ||
| color: $color-smoke-medium | ||
| .value | ||
| float: left | ||
| line-height: 30px | ||
| margin-right: 16px | ||
| font-size: 18px | ||
| font-weight: 600 | ||
| color: $color-smoke-dark | ||
| .opt | ||
| line-height: 30px | ||
| float: left | ||
| button | ||
| color: $color-pr-blue | ||
| &:hover | ||
| background-color: $color-snow-light | ||
| color: $color-pr-blue | ||
|
|
||
| .list | ||
| width: 80% | ||
| margin: 60px auto 0 | ||
| text-align: center | ||
| h3 | ||
| font-size: 18px | ||
| color: $color-smoke-medium | ||
| font-weight: 600 | ||
| margin-bottom: 30px | ||
| ul | ||
| list-style: none | ||
| margin-bottom: 24px | ||
| li | ||
| font-size: 18px | ||
| line-height: 36px | ||
| font-weight: normal | ||
| color: $color-smoke-dark | ||
| margin-bottom: 8px | ||
| cursor: pointer | ||
| &.active, | ||
| &:hover | ||
| background-color: $color-snow-light | ||
| color: $color-pr-blue | ||
| .opts | ||
| display: inline-block | ||
| margin-top: 24px | ||
| .opt | ||
| float: left | ||
| margin: 0 8px | ||
|
|
||
| .progress | ||
| text-align: center | ||
| padding-top: 24px | ||
| .progress-b | ||
| display: inline-block | ||
| .icn | ||
| width: 30px | ||
| height: 30px | ||
| float: left | ||
| .desc | ||
| font-size: 18px | ||
| font-weight: normal | ||
| float: left | ||
| margin-left: 16px | ||
| color: $color-smoke-medium | ||
| &.error | ||
| .icn | ||
| background-image: url($error-url) | ||
| background-position: center | ||
| background-size: contain | ||
| background-repeat: no-repeat | ||
| .desc | ||
| color: $color-error | ||
|
|
||
| .new-chat | ||
| margin-top: 60px | ||
| text-align: center | ||
| h3 | ||
| font-size: 18px | ||
| color: $color-smoke-medium | ||
| font-weight: 600 | ||
| margin-bottom: 30px | ||
| .new-chat-form | ||
| padding-top: 24px | ||
| .inpt | ||
| width: 70% | ||
| margin: 0 auto 24px | ||
| input | ||
| width: 100% | ||
| line-height: 30px | ||
| border: 0 | ||
| border-bottom: 2px solid $color-smoke-light | ||
| font-size: 16px | ||
| outline: 0 | ||
| &:focus | ||
| border-color: $color-pr-blue | ||
| .opts | ||
| display: inline-block | ||
| .opt | ||
| margin: 16px 0 0 | ||
| button.primary-green | ||
| font-size: 18px | ||
| background-color: $color-green | ||
| color: #ffffff | ||
| line-height: 46px | ||
| padding: 0 36px | ||
| &:hover | ||
| background-color: $color-green-dark | ||
|
|
||
| .spinner | ||
| width: 100% | ||
| height: 100% | ||
| background-image: url($spinner-url) | ||
| background-position: center | ||
| background-size: contain | ||
| background-repeat: no-repeat | ||
| animation: rotator .5s infinite linear | ||
|
|
||
|
|
||
| @keyframes rotator | ||
| from | ||
| transform: rotate(0deg) | ||
| to | ||
| transform: rotate(360deg) | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| $FontPathOpenSans: "../../node_modules/npm-font-open-sans/fonts" | ||
| @import '../../node_modules/npm-font-open-sans/open-sans.scss' | ||
| @import 'variables' | ||
| @import 'mixins' | ||
| @import 'button' | ||
| @import 'common' | ||
| @import 'components' | ||
| @import 'chat_room' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| =clearfix() | ||
| &:after | ||
| content: "" | ||
| display: block | ||
| clear: both | ||
| visibility: hidden | ||
|
|
||
| =btnDefault() | ||
| border: 0 | ||
| outline: 0 | ||
| background-color: transparent | ||
| cursor: pointer | ||
| text-transform: uppercase | ||
| &:disabled | ||
| cursor: not-allowed |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
|
|
||
| $logo-lg: 120px | ||
| $logo-sm: 50px | ||
| $logo-xs: 30px | ||
|
|
||
| $color-pr-blue: #5593d7 | ||
| $color-smoke-dark: #555555 | ||
| $color-smoke-medium: #8B8B8B | ||
| $color-smoke-light: #c2c1c1 | ||
| $color-snow-light: #fafafa | ||
| $color-green: #9ED167 | ||
| $color-green-dark: #7AB639 | ||
| $color-logo-desc: $color-pr-blue | ||
| $color-end-call: #F20808 | ||
| $color-end-call-hover: #FD5454 | ||
| $color-error: #FFBBBB | ||
|
|
||
| $body-bg: #fafafa | ||
|
|
||
| $logo-url: '../images/logo.png' | ||
| $error-url: '../images/error.svg' | ||
| $spinner-url: '../images/loader.svg' | ||
| $invite-url: '../images/notification.svg' | ||
| $connect-url: '../images/more.svg' | ||
| $end-call-url: '../images/call.svg' | ||
|
|
||
| $card-width: 650px | ||
| $card-height: 400px | ||
| $card-padd: 24px | ||
|
|
||
| $remote-width: 70% | ||
| $origin-width: 30% |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
|
|
||
| export const stringifyConnInfo = (connInfo) => { | ||
| return JSON.stringify(connInfo); | ||
| }; | ||
|
|
||
| export const parseConnInfo = (connInfo) => { | ||
| return JSON.parse(connInfo); | ||
| }; | ||
|
|
||
| export const putLog = (msg, data) => { | ||
| if (!msg) { | ||
| return; | ||
| } | ||
| // console.log(`${(new Date()).toISOString()} :: ${msg} :: `, data); | ||
| }; | ||
|
|
||
| export const bufToArr = (buf) => { | ||
| if (!(buf instanceof Uint8Array)) { | ||
| throw new Error('buf is not instance of Uint8Array'); | ||
| } | ||
| return Array.from(buf); | ||
| }; | ||
|
|
||
| export const arrToBuf = (arr) => { | ||
| if (!(arr instanceof Array)) { | ||
| throw new Error('arr is not instance of Array'); | ||
| } | ||
| return new Uint8Array(arr); | ||
| }; | ||
|
|
||
| export const uint8ToStr = (buf) => { | ||
| if (!(buf instanceof Uint8Array)) { | ||
| throw new Error('buf is not instance of Uint8Array'); | ||
| } | ||
| return String.fromCharCode.apply(null, new Uint8Array(buf)); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| { | ||
| "name": "safe_webrtc_example", | ||
| "productName": "SAFE WebRTC Example", | ||
| "version": "0.1.0", | ||
| "description": "Application to showcase webRTC secure signalling on SAFE Network", | ||
| "main": "app/main.js", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/maidsafe/safe_examples.git" | ||
| }, | ||
| "author": { | ||
| "name": "MaidSafe", | ||
| "email": "dev@maidsafe.net", | ||
| "url": "https://maidsafe.net" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/maidsafe/safe_examples/issues" | ||
| }, | ||
| "keywords": [ | ||
| "MaidSafe", | ||
| "DemoApp", | ||
| "Safe webRTC", | ||
| "Safe webRTC example" | ||
| ], | ||
| "homepage": "https://github.com/maidsafe/safe_examples#readme", | ||
| "license": "MIT", | ||
| "devDependencies": { | ||
| "babel-core": "^6.26.0", | ||
| "babel-loader": "^7.1.2", | ||
| "babel-plugin-transform-async-to-generator": "^6.24.1", | ||
| "babel-plugin-transform-decorators-legacy": "^1.3.4", | ||
| "babel-plugin-transform-object-rest-spread": "^6.26.0", | ||
| "babel-plugin-transform-runtime": "^6.23.0", | ||
| "babel-polyfill": "^6.26.0", | ||
| "babel-preset-es2015": "^6.24.1", | ||
| "babel-preset-react": "^6.24.1", | ||
| "babel-preset-stage-1": "^6.24.1", | ||
| "css-loader": "^0.28.7", | ||
| "extract-text-webpack-plugin": "^3.0.2", | ||
| "file-loader": "^1.1.5", | ||
| "html-webpack-plugin": "^2.30.1", | ||
| "node-sass": "^4.7.2", | ||
| "sass-loader": "^6.0.6", | ||
| "style-loader": "^0.19.0", | ||
| "url-loader": "^0.6.2", | ||
| "webpack": "^3.9.1", | ||
| "webpack-dev-server": "^2.9.5" | ||
| }, | ||
| "dependencies": { | ||
| "classnames": "^2.2.5", | ||
| "mobx": "^3.3.2", | ||
| "mobx-react": "^4.3.5", | ||
| "mobx-react-router": "^4.0.1", | ||
| "npm-font-open-sans": "^1.1.0", | ||
| "prop-types": "^15.6.0", | ||
| "react": "^16.2.0", | ||
| "react-dom": "^16.2.0", | ||
| "react-route": "^1.0.3", | ||
| "react-router-dom": "^4.2.2" | ||
| }, | ||
| "scripts": { | ||
| "start": "webpack-dev-server --hot --open --config webpack.dev.config.js --content-base ./app", | ||
| "build": "webpack --config webpack.prod.config.js" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| const path = require('path'); | ||
| const webpack = require('webpack'); | ||
| const HtmlWebpackPlugin = require('html-webpack-plugin'); | ||
| const ExtractTextPlugin = require('extract-text-webpack-plugin'); | ||
|
|
||
| module.exports = { | ||
| devtool: 'eval', | ||
| entry: [ | ||
| 'babel-polyfill', | ||
| './main.js', | ||
| './sass/main.sass' | ||
| ], | ||
| context: path.resolve(__dirname, 'app'), | ||
| output: { | ||
| path: path.join(__dirname, 'dist'), | ||
| filename: 'bundle.js', | ||
| publicPath: '/static/' | ||
| }, | ||
| plugins: [ | ||
| new webpack.HotModuleReplacementPlugin(), | ||
| new ExtractTextPlugin({ filename: './styles/style.css', disable: false, allChunks: true }), | ||
| ], | ||
| resolve: { | ||
| extensions: ['.js', '.jsx'] | ||
| }, | ||
| devServer: { | ||
| historyApiFallback: true, | ||
| }, | ||
| module: { | ||
| rules: [ | ||
| { | ||
| test: /\.js?$/, | ||
| use: ['babel-loader'], | ||
| include: path.join(__dirname, 'app') | ||
| }, | ||
| { | ||
| test: /\.sass$/, | ||
| exclude: /node_modules/, | ||
| use: [ | ||
| { loader: "style-loader" }, | ||
| { loader: "css-loader" }, | ||
| { loader: 'sass-loader', query: { sourceMap: false } }, | ||
| ], | ||
| }, | ||
| // SVG Font | ||
| { | ||
| test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, | ||
| use: { | ||
| loader: 'url-loader', | ||
| options: { | ||
| limit: 10000, | ||
| mimetype: 'image/svg+xml', | ||
| } | ||
| } | ||
| }, | ||
| // Common Image Formats | ||
| { | ||
| test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, | ||
| use: 'url-loader', | ||
| }, | ||
| // WOFF Font | ||
| { | ||
| test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, | ||
| use: { | ||
| loader: 'url-loader', | ||
| options: { | ||
| limit: 10000, | ||
| mimetype: 'application/font-woff', | ||
| } | ||
| }, | ||
| }, | ||
| // WOFF2 Font | ||
| { | ||
| test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, | ||
| use: { | ||
| loader: 'url-loader', | ||
| options: { | ||
| limit: 10000, | ||
| mimetype: 'application/font-woff', | ||
| } | ||
| } | ||
| }, | ||
| // TTF Font | ||
| { | ||
| test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, | ||
| use: { | ||
| loader: 'url-loader', | ||
| options: { | ||
| limit: 10000, | ||
| mimetype: 'application/octet-stream' | ||
| } | ||
| } | ||
| }, | ||
| // EOT Font | ||
| { | ||
| test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, | ||
| use: 'file-loader', | ||
| } | ||
| ] | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| const { resolve, join } = require('path'); | ||
| var webpack = require('webpack'); | ||
| const HtmlWebpackPlugin = require('html-webpack-plugin'); | ||
| const ExtractTextPlugin = require('extract-text-webpack-plugin'); | ||
|
|
||
| module.exports = { | ||
| devtool: 'cheap-module-source-map', | ||
| entry: [ | ||
| './main.js', | ||
| './sass/main.sass', | ||
| ], | ||
|
|
||
| context: resolve(__dirname, 'app'), | ||
|
|
||
| output: { | ||
| filename: 'safe-webrtc.js', | ||
| path: resolve(__dirname, 'dist'), | ||
| publicPath: '', | ||
| }, | ||
| plugins: [ | ||
| new webpack.optimize.ModuleConcatenationPlugin(), | ||
| new HtmlWebpackPlugin({ | ||
| template: `${__dirname}/app/index.html`, | ||
| filename: 'index.html', | ||
| inject: 'body', | ||
| }), | ||
| new webpack.optimize.OccurrenceOrderPlugin(), | ||
| new webpack.LoaderOptionsPlugin({ | ||
| minimize: true, | ||
| debug: false, | ||
| }), | ||
| new webpack.optimize.UglifyJsPlugin({ | ||
| beautify: false, | ||
| sourceMap: false | ||
| }), | ||
| new webpack.NoEmitOnErrorsPlugin(), | ||
| new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } }), | ||
| new ExtractTextPlugin({ filename: './styles/style.css', disable: false, allChunks: true }), | ||
| ], | ||
| resolve: { | ||
| extensions: ['.js', '.jsx'] | ||
| }, | ||
| module: { | ||
| rules: [ | ||
| { | ||
| test: /\.js?$/, | ||
| use: ['babel-loader'], | ||
| include: join(__dirname, 'app') | ||
| }, | ||
| { | ||
| test: /\.sass$/, | ||
| exclude: /node_modules/, | ||
| use: [ | ||
| { loader: "style-loader" }, | ||
| { loader: "css-loader" }, | ||
| { loader: 'sass-loader', query: { sourceMap: false } }, | ||
| ], | ||
| }, | ||
| // SVG Font | ||
| { | ||
| test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, | ||
| use: { | ||
| loader: 'url-loader', | ||
| options: { | ||
| name: 'images/[name].[ext]', | ||
| limit: 10000, | ||
| mimetype: 'image/svg+xml', | ||
| } | ||
| } | ||
| }, | ||
| // Common Image Formats | ||
| { | ||
| test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, | ||
| use: 'url-loader', | ||
| }, | ||
| // WOFF Font | ||
| { | ||
| test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, | ||
| use: { | ||
| loader: 'url-loader', | ||
| options: { | ||
| name: 'fonts/[name].[ext]', | ||
| limit: 10000, | ||
| mimetype: 'application/font-woff', | ||
| } | ||
| }, | ||
| }, | ||
| // WOFF2 Font | ||
| { | ||
| test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, | ||
| use: { | ||
| loader: 'url-loader', | ||
| options: { | ||
| name: 'fonts/[name].[ext]', | ||
| limit: 10000, | ||
| mimetype: 'application/font-woff', | ||
| } | ||
| } | ||
| }, | ||
| // TTF Font | ||
| { | ||
| test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, | ||
| use: { | ||
| loader: 'url-loader', | ||
| options: { | ||
| name: 'fonts/[name].[ext]', | ||
| limit: 10000, | ||
| mimetype: 'application/octet-stream' | ||
| } | ||
| } | ||
| }, | ||
| // EOT Font | ||
| { | ||
| test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, | ||
| use: { | ||
| loader: 'file-loader', | ||
| options: { | ||
| name: 'fonts/[name].[ext]', | ||
| } | ||
| }, | ||
| } | ||
| ] | ||
| } | ||
| }; |