53 Commits

Author SHA1 Message Date
650c761c51 Fixed dark theme issues 2019-02-25 07:57:45 +01:00
2e07945994 Block YouTube video by default 2019-02-23 19:08:34 +01:00
d9fe86d160 Added first TypeScript typing rules 2019-02-23 19:08:25 +01:00
bbaaf3a5c2 Only allow one window per conversation 2019-02-23 18:36:40 +01:00
0f4d3042c3 Enabled trickling with WebRTC 2019-02-23 18:30:05 +01:00
be0306b309 Close ring screen if user respond to call on another client 2019-02-23 18:27:31 +01:00
c819aaf716 Fix -1 year ago issue 2019-02-02 10:34:50 +01:00
b6e818dc00 Improved call window size 2019-02-02 10:32:37 +01:00
a3f8e2f243 Get number of members per conversation from API 2019-02-02 08:25:12 +01:00
9f3311978d Block calls for browsers that does not support it. 2019-02-02 08:13:06 +01:00
817ef1069c Can specify whether the signaling server is secure or not. 2019-02-02 08:11:11 +01:00
faba9b36cb Fix build system. 2019-01-26 16:46:36 +01:00
5276790afe Fix ring screen issue 2019-01-26 16:07:57 +01:00
8d613d4e57 Avoid stop loops 2019-01-26 16:03:15 +01:00
5a067001e9 Automatically close ring screen if other peer gives up with connection 2019-01-26 15:59:06 +01:00
1a3117a603 Created calls utilities 2019-01-26 15:49:08 +01:00
cbfe141c32 Play song when the phone is ringing 2019-01-26 15:43:26 +01:00
35c4597017 Can play forever songs 2019-01-26 15:33:43 +01:00
289a66e55b Created song player class 2019-01-26 15:26:49 +01:00
8c6e04abd0 Change cursor on call window title 2019-01-26 12:11:52 +01:00
79dfa0511b Made call system responsive 2019-01-26 12:10:41 +01:00
982c40788c Close more properly local streams. 2019-01-26 11:53:43 +01:00
a46a7154ea Improve dark theme 2019-01-26 11:42:46 +01:00
fe2a6c2dfe Can hide local stream 2019-01-26 11:40:39 +01:00
c1053b8041 Improve window position 2019-01-26 11:11:25 +01:00
194b6c60de Can switch to full screen 2019-01-26 10:57:58 +01:00
0b806d5bb2 Made call window draggable 2019-01-26 10:23:57 +01:00
b8865b96f0 Added footer buttons 2019-01-26 09:52:40 +01:00
b62fefc258 Fix issue 2019-01-26 08:15:17 +01:00
590e1d6794 Automatically close call when a user is the last on a call. 2019-01-26 08:14:17 +01:00
ece7a97d42 Follow API update 2019-01-25 19:18:12 +01:00
99fffb839f Each conversation use its own stream to media and audio stream 2019-01-25 19:03:23 +01:00
154f1b98ef Improved video appearance 2019-01-25 19:03:00 +01:00
45a4f552e8 Got a visual ! 2019-01-25 18:49:50 +01:00
ca39a08d07 Remote stream available ! 2019-01-25 18:30:01 +01:00
5eb1c8b274 Added SimplePeer attribution in README 2019-01-25 17:49:36 +01:00
29b5499b85 Created call window body 2019-01-25 15:34:40 +01:00
b08255cd18 Can detect if a call was rejected 2019-01-25 15:24:32 +01:00
be03249f11 Removed useless source code 2019-01-25 10:13:30 +01:00
0a52bd5fe3 Can reopen calls on page reload 2019-01-25 10:08:55 +01:00
9eab6c7e2e Can respond to calls 2019-01-25 09:43:19 +01:00
118cfeee41 Created ring screen 2019-01-25 09:15:34 +01:00
f79ef55e3b Created a function to easily get the name of a conversation 2019-01-24 18:14:53 +01:00
5640580397 Get user media 2019-01-24 14:50:03 +01:00
e90c20c262 Display basic call window 2019-01-24 14:40:36 +01:00
cd4e6ddcb1 Show call button on conversation of two people when available 2019-01-23 15:45:19 +01:00
feb17e3f13 Automatically retrieve calls configuration 2019-01-23 15:19:34 +01:00
e6f4159e53 Fixed dark theme issues 2019-01-21 09:06:27 +01:00
2d88a71b80 Fix issue 2019-01-21 08:30:56 +01:00
6c090b4967 Bumped to 2019 2019-01-21 07:51:16 +01:00
f67670dc0a Fixed little issue 2019-01-20 19:40:29 +01:00
a03fc1a745 Integrated personnal data navigator 2019-01-20 18:04:09 +01:00
32c484b41a Added personnal data navigator build file 2019-01-20 17:17:13 +01:00
40 changed files with 2555 additions and 67 deletions

View File

@ -1,5 +1,5 @@
MIT License
Copyright (c) 2017-2018 Pierre HUBERT
Copyright (c) 2017-2019 Pierre HUBERT
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -29,3 +29,4 @@ ComunicWeb would not exists without the following technologies developped by the
- SCEditor (BBC WYIWYG editor) (https://github.com/samclarke/SCEditor) (MIT License)
- JavaScript BBCode Parser (https://github.com/Frug/js-bbcode-parser) (MIT License)
- Pacman (https://github.com/daleharvey/pacman) (WTFPL License)
- SimplePeer (https://github.com/feross/simple-peer) (MIT License)

View File

@ -0,0 +1,262 @@
/**
* Signal exchanger web client
*
* @author Pierre HUBERT
*/
class SignalExchangerClient {
/**
* Server domain
*
* @type {String}
*/
//domain;
/**
* Server port
*
* @type {Number}
*/
//port;
/**
* Current client ID
*
* @type {String}
*/
//clientID;
/**
* Socket connection to the server
*
* @type {WebSocket}
*/
//socket;
/**
* Function called in case of error
*
* @type {Function}
*/
//onError = null;
/**
* Function called when the connection is etablished
*
* @type {Function}
*/
//onConnected = null;
/**
* Function called when the connection to the socket is closed
*
* @type {Function}
*/
//onClosed = null;
/**
* Function called when we get a new signal information
*
* @type {Function}
*/
//onSignal = null;
/**
* Function called when we get a ready message notice
*
* @type {Function}
*/
//onReadyMessage = null;
/**
* Construct a client instance
*
* @param {String} domain The name of the signal server
* @param {Number} port The port of the server to use
* @param {String} clientID The ID of current client
* @param {Boolean} secure Specify whether connection to the socket should be secure or not
*/
constructor(domain, port, clientID, secure) {
//Save information
this.domain = domain,
this.port = port;
this.clientID = clientID;
this.socket = new WebSocket((secure ? "wss" : "ws") + "://" + this.domain + ":" + this.port + "/socket");
//Add a few events listeners
this.socket.addEventListener("open", () => {
this.serverConnected();
if(this.onConnected != null)
setTimeout(this.onConnected, 10);
});
this.socket.addEventListener("message", message => {
let data;
try {
data = JSON.parse(message.data);
} catch(e){
console.error("Could not parse message from server!");
return;
}
console.log("New message from socket", data);
this.serverMessage(data);
});
this.socket.addEventListener("error", () => {
if(this.onError != null)
setTimeout(this.onError, 0);
});
this.socket.addEventListener("close", () => {
if(this.onClosed != null)
setTimeout(this.onClosed, 0);
});
}
/**
* Use this method to get the current connection status to the server
*
* @return {Boolean} TRUE if the client is connected to the server / FALSE else
*/
isConnected() {
return this.socket.readyState == WebSocket.OPEN;
}
/**
* Close the connection to the server (if connected)
*/
close() {
if(this.isConnected())
this.socket.close();
}
/**
* Method called once the client is successfully
* connected to the client
*/
serverConnected(){
//Send data to the server to identificate client
this.sendData({
client_id: this.clientID
});
}
/**
* Send ready message to a peer
*
* @param {String} peerID The ID of the target peer for the message
*/
sendReadyMessage(peerID){
this.sendData({
ready_msg: true,
target_id: peerID
});
}
/**
* Send a signal to the server
*
* @param target_id The ID of the target for the signal
* @param content Signal to send to the target
*/
sendSignal(target_id, content){
//Send directly the message to the server
this.sendData({
signal: content,
target_id: target_id
});
//Save the current signal being sent to be able to send
//it again in case of failure
this.pending_signal = content;
this.pending_signal_target = target_id;
}
/**
* Stop to try to send the current signal message in queue
*
* This does not cancel the sending of messages already sent through
* socket
*/
cancelCurrentSignal() {
this.pending_signal = undefined;
this.pending_signal_target = undefined;
}
/**
* Send data to the server
*
* @param {Object} data The data to send to the server
*/
sendData(data){
console.log("Sending data to server", data);
this.socket.send(JSON.stringify(data));
}
/**
* This method is called when the server has sent a new message to this client
*
* @param {Object} message The message sent by the server, as a JSON object
*/
serverMessage(message){
//Check if it is a callback for a pending message
if(message.signal_sent){
if(message.number_of_targets < 1 && this.pending_signal && this.pending_signal_target){
//We have to send the message again
setTimeout(() => {
this.sendSignal(this.pending_signal, this.pending_signal_target);
}, 1000);
}
else {
//Else we can remove from this class information about the signal being sent
this.cancelCurrentSignal();
}
}
//Check if message is a callback for a ready notice
else if(message.ready_message_sent){
if(message.number_of_targets < 1){
//Try to send message again
setTimeout(() => {
this.sendReadyMessage(message.target_id);
}, 1000);
}
}
// Check if message is a ready notice
else if(message.ready_msg){
if(this.onReadyMessage != null)
this.onReadyMessage(message.source_id);
}
// Check if the message is a signal
else if(message.signal){
if(this.onSignal != null)
this.onSignal(message.signal, message.source_id);
}
}
}

File diff suppressed because one or more lines are too long

BIN
assets/audio/phone_ring.mp3 Normal file

Binary file not shown.

BIN
assets/audio/phone_ring.ogg Normal file

Binary file not shown.

View File

@ -25,6 +25,10 @@ a {
color: #72afd2;
}
.cursor-pointer {
cursor: pointer;
}
/**
* Sceditor iframe
*/

View File

@ -0,0 +1,158 @@
/**
* Calls window stylesheet
*
* @author Pierre HUBERT
*/
#callsTarget {
position: fixed;
width: 100%;
height: 100%;
top: 0px;
visibility: hidden;
z-index: 1000;
}
.call-window {
width: 300px;
min-width: 300px;
min-height: 174px;
position: absolute;
top: 100px;
right: 10px;
z-index: 100;
border: 1px black solid;
display: flex;
flex-direction: column;
background-color: #000000b3;
visibility: visible;
}
.call-window .call-window-body{
flex: 1;
display: flex;
max-height: calc(100% - 62px);
}
/**
* Toolbar
*/
.call-toolbar {
height: 20px;
background-color: #000000b3;
color: white;
display: flex;
align-items: center;
padding-left: 10px;
}
.call-toolbar > i:first-child {
margin-right: 6px;
}
.call-title {
flex: 1;
cursor: move;
}
/**
* Loading message
*/
.loading-message-container {
position: absolute;
min-height: 112px;
width: 100%;
display: flex;
background: 1px #0009;
flex-direction: column;
justify-content: space-around;
text-align: center;
font-size: 150%;
color: white;
}
/**
* Stream target
*/
.call-window .streams-target {
flex: 1;
display: flex;
flex-wrap: wrap;
flex-direction: row;
max-width: 100%;
}
.call-window .streams-target video {
flex: 1;
flex-shrink: 1;
max-height: 100%;
}
/**
* Footer
*/
.call-window .call-footer {
height: 40px;
display: flex;
justify-content: space-around;
align-items: center;
}
.call-window .call-footer .call-option-button {
color: #fff6;
flex: 1;
text-align: center;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.call-window .call-footer .call-option-button.selected {
color: white;
}
.call-window .call-footer .call-option-button:hover {
background-color: #ffffff4d;
}
.call-window .call-footer .call-option-button:active {
background-color: #fff3;
}
.call-window .call-footer .call-option-button.hang-up-button {
color: #dd4b39;
}
/**
* Responsive mode
*/
@media screen and (max-width: 730px) {
#callsTarget {
z-index: 1030;
}
.streams-target {
flex-direction: column !important;
}
.streams-target video {
max-width: 100%;
}
.call-window {
position: fixed;
width: 100%;
height: 100%;
top: 0px !important;
left: 0px !important;
}
}

View File

@ -0,0 +1,33 @@
/**
* Ring screen stylesheet
*
* @author Pierre HUBERT
*/
.ring-call-container {
width: 100%;
height: 100%;
position: fixed;
top: 0px;
left: 0px;
background-color: #011932e6;
z-index: 1030;
display: flex;
justify-content: center;
align-items: center;
}
.call-message-box {
background-color: #fff6;
color: white;
padding: 10px;
border-radius: 5px;
min-width: 200px;
text-align: center;
font-size: 150%;
}
.ring-call-container .response-buttons {
display: flex;
justify-content: space-around;
}

View File

@ -4,6 +4,10 @@
* @author Pierre HUBERT
*/
/**
* Main definition
*/
#conversationsElem .box {
width: 250px;
height: 350px;
@ -15,6 +19,10 @@
margin-bottom: 0px;
}
#conversationsElem .chat-window .box-title {
font-size: 15px !important;
}
/**
* Conversations create message form
*/

View File

@ -63,6 +63,20 @@
text-align: center;
}
.post .post-youtube.post-youtube-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
background-color: #3c8dbc;
color: white;
border-radius: 5px;
}
.post .post-youtube.post-youtube-placeholder .title {
font-size: 150%;
}
.post .post-youtube {
width: 100%;
height: 300px;

View File

@ -58,6 +58,14 @@ p, h1, h2, h3, h4, h5, h6 {
}
/**
* Callouts
*/
.callout.callout-info {
background-color: var(--black-5) !important;
}
/**
* Forms
*/
@ -346,11 +354,16 @@ div.sceditor-dropdown input {
color: var(--black5);
}
.select2-dropdown {
background-color: var(--black5);
.select2-dropdown, .select2-selection,
.select2-search__field {
background-color: var(--black5) !important;
color: var(--white);
}
.select2-selection__rendered {
color: var(--white) !important;
}
.select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: var(--black4);
color: var(--white);
@ -437,6 +450,10 @@ div.sceditor-dropdown input {
filter: brightness(100%);
}
.post-youtube-placeholder {
background-color: var(--black6) !important;
}
.box-comments .username,
.box-comments .comment-content {
color: var(--white);
@ -557,3 +574,53 @@ img[src$="groups_logo/default.png"] {
.friends-list-ro .friend a:hover .friends-name {
color: var(--black4);
}
/**
* Account created page
*/
.page_account_created .message_container .message {
background-color: var(--black5);
}
/**
* Call system
*/
.call-window {
border: 1px var(--black4) solid;
}
.ring-call-container {
background-color: var(--black5);
}
.call-message-box {
background-color: var(--black6);
}
/**
* Groups list page
*/
.groups-main-page {
color: var(--white);
}
/**
* Groups members page
*/
.group-members-page {
color: var(--white);
}
/**
* Pick a movie modal
*/
.pick-movie-modal {
color: var(--white);
}
/**
* Group settings page
*/
.group-settings-container label {
color: var(--white);
}

35
assets/custom_ts/Utils.d.ts vendored Normal file
View File

@ -0,0 +1,35 @@
/**
* Typescript typing rules for Utils
*
* @author Pierre HUBERT
*/
declare interface CreateElem2Args {
type : string,
appendTo ?: HTMLElement,
insertBefore ?: HTMLElement,
insertAsFirstChild ?: HTMLElement,
class ?: string,
id ?: string,
title ?: string,
src ?: string,
href ?: string,
name ?: string,
elemType ?: string,
value ?: string,
placeholder ?: string,
innerHTML ?: string,
innerLang ?: string,
innerHTMLprefix ?: string,
disabled ?: boolean,
}
declare function createElem(nodeType : string, appendTo : string) : HTMLElement;
declare function createElem2(infos : CreateElem2Args) : HTMLElement;
declare function byId(id : string) : HTMLElement;
declare function emptyElem(target : HTMLElement) : void;
declare function checkMail(emailAddress : string) : boolean;

View File

@ -21,6 +21,11 @@ ComunicWeb.common.date = {
* @return {String} The generated date
*/
diffToStr: function(difference){
//Check if difference is less than one second
if(difference < 0)
difference = 0;
//Calculate seconds
var seconds = difference-Math.floor(difference/60)*60;
var difference = (difference - seconds)/60;

View File

@ -1144,6 +1144,66 @@ var ComunicWeb = {
},
/**
* Calls component
*/
calls: {
/**
* Calls configuration
*/
__config: undefined,
/**
* Calls interface
*/
interface: {
//TODO : implement
},
/**
* Calls controller
*/
controller: {
//TODO : implement
},
/**
* Call window
*/
callWindow: {
//TODO : implement
},
/**
* Current calls list
*/
currentList: {
//TODO : implement
},
/**
* User media getter
*/
userMedia: {
//TODO : implement
},
/**
* Ring screen
*/
ringScreen: {
//TODO : implement
},
/**
* Calls utilities
*/
utils: {
//TODO : implement
},
},
/**
* Easter egg : pacman
*/

View File

@ -0,0 +1,51 @@
/**
* Song player
*
* @author Pierre HUBERT
*/
class SongPlayer {
/**
* Initialize a new SongPlayer instance
*
* @param {String[]} sources The list of sources to exploit for the song
*/
constructor(sources){
this.songElem = document.createElement("audio");
//Process the list of sources
for (var index = 0; index < sources.length; index++) {
var url = sources[index];
var source = document.createElement("source");
source.src = url;
this.songElem.appendChild(source);
}
}
/**
* Play audio just once
*/
playOnce(){
this.songElem.loop = false;
this.songElem.play();
}
/**
* Play song forever
*/
playForever(){
this.songElem.loop = true;
this.songElem.play();
}
/**
* Stop song
*/
stop(){
this.songElem.pause();
this.songElem.currentTime = 0;
}
}

View File

@ -58,6 +58,11 @@ ComunicWeb.common.system = {
*/
ComunicWeb.components.darkTheme.refresh();
/**
* Initialize call system
*/
ComunicWeb.components.calls.controller.init();
/**
* What to do after login refresh
*/

View File

@ -421,7 +421,7 @@ function checkString(value){
function removeHtmlTags(input){
//Check if input string is empty
if(input == null)
if(input == null || typeof input !== "string")
return "";
//Prepare update
@ -701,3 +701,56 @@ function SendEvent(name, details){
document.dispatchEvent(event);
}
/**
* Request full screen for an HTML element
*
* Note : this function must be called inside of an event of
* the user like a click, otherwise it will not work
*
* This function source code is based on this StackOverFlow Answer
* https://stackoverflow.com/a/32100295/3781411
*
* @param {HTMLElement} elem The element for which we want
* full screen
*/
function RequestFullScreen(element){
if (
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement
) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
} else {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
}
}
/**
* Check out whether Comunic has grabbed full screen or not
*
* @return {Boolean} TRUE if the window is in full screen / FALSE else
*/
function IsFullScreen(){
return document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement;
}

View File

@ -25,18 +25,51 @@ ComunicWeb.components.account.export.worker = {
ComunicWeb.components.account.export.ui.updateMessage("Got text data");
ComunicWeb.components.account.export.ui.updateProgress(10);
//Parse data
ComunicWeb.components.account.export.worker.parse(result);
//Get explorer
ComunicWeb.components.account.export.worker.getExplorer(result);
});
},
/**
* Second step for export : Get and open personnal data explorer
*
* @param {Object} data Text data about the account (data not modified at this stage)
*/
getExplorer: function(data){
ComunicWeb.components.account.export.ui.updateMessage("Getting data explorer");
ComunicWeb.components.account.export.ui.updateProgress(15);
JSZipUtils.getBinaryContent(ComunicWeb.__config.assetsURL+"zip/personnal-data-export-navigator.zip", function(err, file){
if(err != null){
ComunicWeb.debug.logMessage("Could not get personnal data export navigator!");
ComunicWeb.components.account.export.ui.exportFatalError(e);
return;
}
JSZip.loadAsync(file).then(function(zip){
//Ready to parse data
ComunicWeb.components.account.export.worker.parse(data, zip);
}).catch(function(){
ComunicWeb.debug.logMessage("Could not parse personnal data export navigator!");
ComunicWeb.components.account.export.ui.exportFatalError(e);
return;
});
});
},
/**
* Parse account text data into ZIP file
*
* @param {Object} data Text data about the account
* @param {ZIP} zip The ZIP object to fill
*/
parse: function(data){
parse: function(data, zip){
//Get UI shorcut
var ui = ComunicWeb.components.account.export.ui;
@ -77,9 +110,6 @@ ComunicWeb.components.account.export.worker = {
//Determine the list of files to download
var files_list = this._generate_files_list(data);
//Create zip file
var zip = new JSZip();
//Add raw json file
zip.file("source.json", JSON.stringify(data));

View File

@ -0,0 +1,838 @@
/**
* Single call window management
*
* @author Pierre HUBERT
*/
ComunicWeb.components.calls.callWindow = {
/**
* Initialize a call
*
* @param {Object} info Information about the call to initialize
*/
initCall: function(info){
/**
* Initialize call object
*/
var call = {
info: info,
/**
* @type {String}
*/
localPeerID: undefined,
/**
* @type {Boolean}
*/
open: true,
/**
* @type {Boolean}
*/
stopped: false,
window: {},
streams: {},
/**
* @type {MediaStream}
*/
localStream: undefined,
/**
* @type {HTMLVideoElement}
*/
localStreamVideo: undefined,
/**
* @type {SignalExchangerClient}
*/
signalClient: undefined
};
/**
* Initialize utilities
*/
/**
* Get member information based on call ID
*
* @param {String} id The ID of the peer to process
* @return Information about the peer / empty object
* if no peer found
*/
call.getMemberByCallID = function(id){
var memberInfo = undefined;
call.info.members.forEach(function(member){
if(member.user_call_id == id)
memberInfo = member;
});
return memberInfo;
}
/**
* Check out whether local stream audio is enabled or not
*
* @return {Boolean} TRUE if local stream is enabled / FALSE else
*/
call.isLocalAudioEnabled = function(){
//Default behaviour : local stream not enabled
if(!call.localStream)
return true;
return call.localStream.getAudioTracks()[0].enabled;
}
/**
* Set audio mute status of local stream
*
* @param {Boolean} enabled New enabled status
*/
call.setLocalAudioEnabled = function(enabled){
if(call.localStream)
call.localStream.getAudioTracks()[0].enabled = enabled;
}
/**
* Check if the local video is enabled or not
*
* @return {Boolean} TRUE to mute video / FALSE else
*/
call.isLocalVideoEnabled = function(){
//Default behaviour : video not enabled
if(!call.localStream)
return true;
return call.localStream.getVideoTracks()[0].enabled;
}
/**
* Update mute status of local video stream
*
* @param {Boolean} enabled New mute status
*/
call.setLocalVideoEnabled = function(enabled){
if(call.localStream)
call.localStream.getVideoTracks()[0].enabled = enabled;
}
/**
* Set local stream video visibility
*
* @param {Boolean} visible TRUE to make it visible / FALSE else
*/
call.setLocalStreamVisibility = function(visible){
if(call.localStreamVideo)
call.localStreamVideo.style.display = visible ? "block" : "none";
}
/**
* Get local stream visibility
*
* @return {Boolean} TRUE if local stream is visible / FALSE else
*/
call.isLocalStreamVisible = function(){
if(!call.localStreamVideo)
return true;
return call.localStreamVideo.style.display !== "none";
}
/**
* We have to begin to draw conversation UI
*/
var callContainer = createElem2({
appendTo: byId("callsTarget") ? byId("callsTarget") : byId("wrapper"), //If call target is not found, add call in page wrapper
type: "div",
class: "call-window"
});
call.window.container = callContainer;
//Add toolbar
call.window.toolbar = createElem2({
appendTo: callContainer,
type: "div",
class: "call-toolbar",
innerHTML: "<i class='fa fa-phone'>"
});
//Call title
call.window.title = createElem2({
appendTo: call.window.toolbar,
type: "div",
class: "call-title",
innerHTML: "Loading..."
});
/**
* Update the title of the call
*/
call.setTitle = function(title){
call.window.title.innerHTML = title;
}
//Add close button
call.window.closeButton = createElem2({
appendTo: call.window.toolbar,
type: "button",
class: "btn btn-box-tool close-btn",
innerHTML: "<i class='fa fa-times'></i>"
});
//Make close button lives
call.close = function(){
//Avoid to call this several times
if(call.stopped)
return;
call.open = false;
call.stopped = true;
callContainer.remove();
//Close sockets connections too
ComunicWeb.components.calls.callWindow.stop(call);
}
call.window.closeButton.addEventListener("click", function(){
//Check if the call is in full screen mode
if(IsFullScreen())
RequestFullScreen(null)
else
call.close();
});
//Get information about related conversation to get the name of the call
ComunicWeb.components.conversations.utils.getNameForID(info.conversation_id, function(name){
if(!name)
return notify("Could not get information about related conversation!", "danger");
call.setTitle(name);
});
//Call box body
call.window.body = createElem2({
appendTo: callContainer,
type: "div",
class: "call-window-body"
});
//Call videos target
call.window.videosTarget = createElem2({
appendTo: call.window.body,
type: "div",
class: "streams-target"
});
/**
* Create loading message area
*/
call.window.loadingMessageContainer = createElem2({
insertBefore: call.window.body.firstChild,
type: "div",
class: "loading-message-container",
innerHTML: "<i class='fa fa-clock-o'></i>"
});
call.window.loadingMessageContent = createElem2({
appendTo: call.window.loadingMessageContainer,
type: "div",
class: "message",
innerHTML: "Loading..."
});
/**
* Set loading message visiblity
*
* @param {Boolean} visible TRUE to make it visible / FALSE else
*/
call.setLoadingMessageVisibility = function(visible){
call.window.loadingMessageContainer.style.display = visible ? "flex" : "none";
}
/**
* Update call loading message
*
* @param {String} message The new message to show to the
* users
*/
call.setLoadingMessage = function(message){
call.setLoadingMessageVisibility(true);
call.window.loadingMessageContent.innerHTML = message;
}
//Call footer
call.window.footer = createElem2({
appendTo: callContainer,
type: "div",
class: "call-footer"
});
/**
* This function is used to toggle selection state
* of one of the call toolbar button
*
* @param {HTMLElement} btn Target button
* @param {Boolean} selected Selection state of the button
*/
var togglButtonSelectedStatus = function(btn, selected){
if(!selected){
while(btn.className.includes(" selected"))
btn.className = btn.className.replace(" selected", "");
}
else if(selected && !btn.className.includes(" selected"))
btn.className += " selected";
}
var buttonsList = [
//Show current user camera
{
icon: "fa-eye",
selected: true,
onclick: function(btn){
call.setLocalStreamVisibility(!call.isLocalStreamVisible());
togglButtonSelectedStatus(btn, call.isLocalStreamVisible());
}
},
//Mute button
{
icon: "fa-microphone",
selected: true,
onclick: function(btn){
call.setLocalAudioEnabled(!call.isLocalAudioEnabled());
togglButtonSelectedStatus(btn, call.isLocalAudioEnabled());
}
},
//Hang up button
{
icon: "fa-phone",
class: "hang-up-button",
selected: false,
onclick: function(){
call.close();
}
},
//Stop video button
{
icon: "fa-video-camera",
selected: true,
onclick: function(btn){
call.setLocalVideoEnabled(!call.isLocalVideoEnabled());
togglButtonSelectedStatus(btn, call.isLocalVideoEnabled());
console.log(call);
}
},
//Full screen button
{
icon: "fa-expand",
selected: false,
onclick: function(btn){
RequestFullScreen(callContainer);
setTimeout(function(){
togglButtonSelectedStatus(btn, IsFullScreen());
}, 1000);
}
}
];
//Add buttons
buttonsList.forEach(function(button){
var buttonEl = createElem2({
appendTo: call.window.footer,
type: "div",
class: "call-option-button",
innerHTML: "<i class='fa " + button.icon + "'></i>"
});
//Add button optionnal class
if(button.class)
buttonEl.className += " " + button.class;
buttonEl.addEventListener("click", function(){
button.onclick(buttonEl);
});
togglButtonSelectedStatus(buttonEl, button.selected);
});
/**
* Make the call window draggable
*/
function checkWindowMinPosition(){
if(window.innerHeight < callContainer.style.top.replace("px", ""))
callContainer.style.top = "0px";
if(window.innerWidth < callContainer.style.left.replace("px", ""))
callContainer.style.left = "0px";
if(callContainer.style.left.replace("px", "") < 0)
callContainer.style.left = "0px";
if(callContainer.style.top.replace("px", "") < 49)
callContainer.style.top = "50px";
}
//Enable dragging
{
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
call.window.title.onmousedown = function(e){
e = e || window.event;
e.preventDefault();
//Check if the window is currently in full screen mode
if(IsFullScreen())
return;
//get the mouse cursor position at startup
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e){
e = e || window.event;
e.preventDefault();
//Calculate new cursor position
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
//Set element new position
callContainer.style.top = (callContainer.offsetTop - pos2) + "px";
callContainer.style.left = (callContainer.offsetLeft - pos1) + "px";
checkWindowMinPosition();
}
function closeDragElement(){
//Stop moving when mouse button is released
document.onmouseup = null;
document.onmousemove = null;
}
}
window.addEventListener("resize", function(){
checkWindowMinPosition();
});
//Load user media
call.setLoadingMessage("Waiting for your microphone and camera...");
ComunicWeb.components.calls.userMedia.get().then(function(stream){
//Check if connection has already been closed
if(!call.open)
return;
call.localStream = stream;
//Initialize signaling server connection
ComunicWeb.components.calls.callWindow.initializeConnectionToSignalingServer(call);
//Add local stream to the list of visible stream
call.localStreamVideo = ComunicWeb.components.calls.callWindow.addVideoStream(call, true, stream);
//Mark as connecting
call.setLoadingMessage("Connecting...");
return true;
}).catch(function(e){
console.error("Get user media error: ", e);
call.setLoadingMessageVisibility(false);
return notify("Could not get your microphone and camera!", "danger");
});
/**
* Start to automaticaly refresh information about the call
*/
var interval = setInterval(function(){
//Check if call is not visible anymore
if(!callContainer.isConnected){
call.close();
return;
}
if(!call.open)
return clearInterval(interval);
ComunicWeb.components.calls.callWindow.refreshInfo(call);
}, 4000);
},
/**
* Initialize connection to signaling server
*
* @param {Object} call Information about the call
*/
initializeConnectionToSignalingServer: function(call) {
//Get current user call ID
call.info.members.forEach(function(member){
if(member.userID == userID())
call.localPeerID = member.user_call_id;
});
//Create client instance and connect to server
var config = ComunicWeb.components.calls.controller.getConfig();
call.signalClient = new SignalExchangerClient(
config.signal_server_name,
config.signal_server_port,
call.localPeerID,
config.is_signal_server_secure
);
/**
* Error when connecting to signaling server
*/
call.signalClient.onError = function(){
call.setLoadingMessage("Could not connect to signaling server!");
call.open = false;
};
/**
* Connection to signaling server is not supposed to close
*/
call.signalClient.onClosed = function(){
call.setLoadingMessage("Connection to signaling server closed!");
call.open = false;
}
/**
* A remote peer sent a ready notice
*/
call.signalClient.onReadyMessage = function(peerID){
ComunicWeb.components.calls.callWindow.readyToInitialize(call, peerID);
}
/**
* A remote peer sent a signal
*/
call.signalClient.onSignal = function(signal, peerID){
ComunicWeb.components.calls.callWindow.receivedSignal(call, peerID, signal);
}
},
/**
* Refresh at a regular interval information about the call
*
* @param {Object} call Call Root object
*/
refreshInfo: function(call){
ComunicWeb.components.calls.interface.getInfo(call.info.id, function(result){
if(result.error)
return notify("Could not get information about the call!", "danger");
call.info = result;
ComunicWeb.components.calls.callWindow.gotNewCallInfo(call);
});
},
/**
* This method get called each time information about the call
* are refreshed on the server
*
* @param {Object} call Information about the call
*/
gotNewCallInfo: function(call) {
//Check if we are connected to signaling server and we have got local
//streams
if(!call.signalClient || !call.signalClient.isConnected() || !call.localStream)
return;
//Check if all other members rejected call
var allDisconnected = ComunicWeb.components.calls.utils.hasEveryoneLeft(call.info);
//Check if all call peer rejected the call
if(allDisconnected){
call.setLoadingMessage("Conversation terminated.");
setTimeout(function(){
call.close();
}, 5000);
return;
}
//Process the connection to each accepted member
call.info.members.forEach(function(member){
//Ignores all not accepted connection
if(member.userID == userID() || member.status !== "accepted")
return;
//Check if we have not peer information
if(!call.streams.hasOwnProperty("peer-" + member.userID)){
//If the ID of the current user is bigger than the remote
//peer user ID, we wait a ready signal from him
if(member.userID < userID())
return;
//Else we have to create peer
ComunicWeb.components.calls.callWindow.createPeerConnection(call, member, false);
}
});
},
/**
* Create a peer connection
*
* @param {Object} call Information about the peer
* @param {Object} member Information about the member
* @param {Boolean} isInitiator Specify whether current user is the
* initiator of the connection or not
*/
createPeerConnection: function(call, member, isInitiator){
var peerConnection = {
peer: undefined
};
call.streams["peer-" + member.userID] = peerConnection;
//Get call configuration
var config = ComunicWeb.components.calls.controller.getConfig();
//Create peer
var peer = new SimplePeer({
initiator: isInitiator,
stream: call.localStream,
trickle: true,
config: {
'iceServers': [
{ urls: config.stun_server },
{"url": config.turn_server,
"credential": config.turn_username,
"username": config.turn_password}
]
}
});
peerConnection.peer = peer;
//Add a function to remove connection
peerConnection.removePeerConnection = function(){
peer.destroy();
delete call.streams["peer-" + member.userID];
if(peerConnection.video)
peerConnection.video.remove();
}
peer.on("error", function(err){
console.error("Peer error !", err, member);
peerConnection.removePeerConnection();
});
peer.on("signal", function(data){
console.log('SIGNAL', JSON.stringify(data));
call.signalClient.sendSignal(member.user_call_id, JSON.stringify(data));
});
peer.on("message", function(message){
console.log("Message from remote peer: " + message);
});
peer.on("close", function(){
peerConnection.removePeerConnection();
});
peer.on("stream", function(stream){
ComunicWeb.components.calls.callWindow.streamAvailable(call, member, stream);
});
//If this peer does not initialize connection, inform other peer we are ready
if(!isInitiator)
call.signalClient.sendReadyMessage(member.user_call_id);
},
/**
* This method is called when a remote peers notify it is ready to
* establish connection
*
* @param {Object} call Information about the call
* @param {String} peerID Remote peer ID
*/
readyToInitialize: function(call, peerID){
var member = call.getMemberByCallID(peerID);
if(member == undefined)
return;
//It the user with the smallest ID who send the ready message
//else it would mess everything up
if(member.userID > userID())
return;
this.createPeerConnection(call, member, true);
},
/**
* This method is called when we received a remote signal
*
* @param {Object} call Information about the call
* @param {String} peerID Remote peer ID
* @param {String} signal Received signal
*/
receivedSignal: function(call, peerID, signal){
console.log("Received signal from " + peerID, signal);
var member = call.getMemberByCallID(peerID);
if(member == undefined)
return;
//Check we have got peer information
if(!call.streams.hasOwnProperty("peer-" + member.userID))
return;
call.streams["peer-" + member.userID].peer.signal(JSON.parse(signal));
},
/**
* This method is called when a remote stream becomes available
*
* @param {Object} call Information about remote call
* @param {String} member Information about target member
* @param {MediaStream} stream Remote stream available
*/
streamAvailable: function(call, member, stream){
call.setLoadingMessageVisibility(false);
call.streams["peer-" + member.userID].stream = stream;
call.streams["peer-" + member.userID].video = this.addVideoStream(call, false, stream);
},
/**
* Create and set a video object for a stream
*
* @param {Object} call Target call
* @param {Boolean} muted Specify whether audio should be muted
* or not
* @param {MediaStream} stream Target stream
* @return {HTMLVideoElement} Generated video element
*/
addVideoStream: function(call, muted, stream){
/**
* @type {HTMLVideoElement}
*/
var video = createElem2({
appendTo: call.window.videosTarget,
type: "video"
});
video.muted = muted;
//Set target video object and play it
video.srcObject = stream;
video.play();
return video;
},
/**
* Stop the ongoing call
*
* @param {Object} call Information about the call
*/
stop: function(call){
//Remove the call from the opened list
ComunicWeb.components.calls.currentList.removeCallFromList(call.info.id);
//Close the connection to the server
if(call.signalClient && call.signalClient.isConnected())
call.signalClient.close();
//Close all socket connections
for (var key in call.streams) {
if (call.streams.hasOwnProperty(key)) {
var element = call.streams[key];
element.removePeerConnection();
}
}
//Close local stream
if(call.localStream){
call.localStream.getTracks().forEach(function(track){
track.stop();
});
}
//Notify server
ComunicWeb.components.calls.interface.hangUp(call.info.id, function(){});
}
}

View File

@ -0,0 +1,293 @@
/**
* Calls controller
*
* @author Pierre HUBERT
*/
ComunicWeb.components.calls.controller = {
/**
* This variable contains the initialization state
* of the call component
*/
_is_init: false,
/**
* This variable contains whether the user is being
* notified of a call or not
*/
_is_processing_call: false,
/**
* Initialize calls component
*/
init: function(){
//We init this component just once
if(this._is_init)
return;
ComunicWeb.debug.logMessage("Initialize calls component");
//Initialize call container
var initializeCallContainer = function(){
//Signed out users can not make calls
if(!signed_in()){
ComunicWeb.components.calls.controller.userSignedOut();
return;
}
//Need a wrapper to continue
if(!byId("wrapper"))
return;
//Check if calls target already exists
if(byId("callsTarget"))
return;
//Call system must be available
if(!ComunicWeb.components.calls.controller.isAvailable())
return;
//Create call target
createElem2({
appendTo: byId("wrapper"),
type: "div",
id: "callsTarget"
});
//Now we have to reopen current calls
ComunicWeb.components.calls.controller.reopenCurrentCalls();
}
//We wait the user to be connected before trying to get
// call configuration
document.addEventListener("got_user_id", function(){
//Check if we have already the call configuration
if(ComunicWeb.components.calls.__config !== undefined)
return;
ComunicWeb.components.calls.interface.getConfig(function(config){
//Check if we could not get calls configuration
if(config.error)
return;
//Save calls configuration
ComunicWeb.components.calls.__config = config;
initializeCallContainer();
});
});
// Each time a page is opened, wec check if we have to create calls target
document.addEventListener("openPage", function(){
initializeCallContainer();
});
},
/**
* Access calls configuration
*
* @return Cached calls configuration
*/
getConfig: function() {
return ComunicWeb.components.calls.__config;
},
/**
* Check if the call feature is available or not
*/
isAvailable: function(){
//First, check if this browser support webrtc
if(!SimplePeer.WEBRTC_SUPPORT)
return false;
//If do not have any call configuration, it is not possible to
//make calls
if(this.getConfig() == null)
return false;
//Read configuration
return this.getConfig().enabled;
},
/**
* Initiate a call for a conversation
*
* @param {Number} conversationID The ID of the target conversation
*/
call: function(conversationID){
//Create / Get call information for the conversation
ComunicWeb.components.calls.interface.createForConversation(conversationID, function(call){
if(call.error)
return notify("Could not get a call for this conversation!", "danger");
ComunicWeb.components.calls.controller.open(call);
});
},
/**
* Call this method to initialize a call for a call we have information about
*
* @param {Object} call Information about the call
*/
open: function(call){
//Check if the call is already in the list of calls
if(ComunicWeb.components.calls.currentList.isCallInList(call.id))
return;
//Add the call to the list of opened calls
ComunicWeb.components.calls.currentList.addCallToList(call.id);
//Initialize call
ComunicWeb.components.calls.callWindow.initCall(call);
},
/**
* This method is called each time the notification service
* detect that the number of pending calls has increased. It
* must in fact be "thread-safe" to avoid to do twice things
* that should be one only once
*
* @param {number} number The number of pending calls
*/
newCallsAvailable: function(number){
//Check if user is already processing a call
if(this._is_processing_call)
return;
this._is_processing_call = true;
/**
* Switch processing call to false
*/
var undoIsProcessing = function(){
ComunicWeb.components.calls.controller._is_processing_call = false;
}
//Get information about the next pending call
ComunicWeb.components.calls.interface.getNextPendingCall(function(call){
//Check if there is no pending call
if(call.notice)
return undoIsProcessing();
ComunicWeb.components.conversations.utils.getNameForID(call.conversation_id, function(name){
//Check for errors
if(!name){
ComunicWeb.debug.logMessage("Could not get the name of the conversation for a call, cannot process it!");
undoIsProcessing();
return;
}
//Show ring screen
var prompting = true;
var ringScreenInfo = ComunicWeb.components.calls.ringScreen.show(name, 30, function(accept){
prompting = false;
undoIsProcessing();
ComunicWeb.components.calls.controller.applyReponseForCall(call, accept);
});
//Regulary check if the call is still valid
var interval = setInterval(function(){
if(!prompting)
return clearInterval(interval);
ComunicWeb.components.calls.interface.getInfo(call.id, function(info){
//Check for errors
if(info.error)
return;
//Refuse the call if everyone has left it
if(ComunicWeb.components.calls.utils.hasEveryoneLeft(info))
ringScreenInfo.respond(false);
//Close ring screen if user responded to the call on another Comunic client
if(ComunicWeb.components.calls.utils.getCurrentUserState(info) != "unknown")
ringScreenInfo.close();
});
}, 2000);
});
});
},
/**
* Apply a response for the call
*
* @param {Object} call Information about the target call
* @param {Boolean} accept TRUE to accept call / FALSE else
*/
applyReponseForCall: function(call, accept){
//Send response to server
ComunicWeb.components.calls.interface.respondToCall(call.id, accept, function(r){
//Check for error
if(r.error)
return notify("Could not send response to call to server!", "danger");
if(!accept)
return;
//We may start the call now
ComunicWeb.components.calls.controller.open(call);
});
},
/**
* Reopen all current calls
*/
reopenCurrentCalls: function(){
//Process each call to open it
ComunicWeb.components.calls.currentList.getCurrentCallsList().forEach(function(entry){
ComunicWeb.components.calls.interface.getInfo(entry, function(call){
if(call.error){
ComunicWeb.components.calls.currentList.removeCallFromList(entry);
return notify("Could not get information about a call!", "danger");
}
ComunicWeb.components.calls.controller.open(call);
});
});
},
/**
* Call this method only if the system is sure that
* nobody is signed in the current tab
*/
userSignedOut: function(){
//Remove all the current calls from the list
ComunicWeb.components.calls.currentList.removeAllCalls();
}
}

View File

@ -0,0 +1,84 @@
/**
* Currents calls list management
*
* @author Pierre HUBERT
*/
ComunicWeb.components.calls.currentList = {
/**
* This variable contains the name of the session storage
* variable that contains active calls
*/
_local_storage_list_calls_name: "current-calls",
/**
* Get the list of active calls
*
* @return {number[]} The list of calls
*/
getCurrentCallsList: function(){
var string = localStorage.getItem(this._local_storage_list_calls_name);
if(string === null || string == "")
return [];
else
return string.split(",");
},
/**
* Check if a call ID is in the list of opened calls
*
* @param {Number} id The ID of the call to check
*/
isCallInList: function(id){
return this.getCurrentCallsList().includes(""+id);
},
/**
* Save a new list of calls
*
* @param {number[]} list The new list of calls to save
*/
saveNewCallsList: function(list){
localStorage.setItem(this._local_storage_list_calls_name, list.join(","));
},
/**
* Add a call to the list of opened call
*
* @param {number} id The ID of the call to add
*/
addCallToList: function(id){
var list = this.getCurrentCallsList();
if(!list.includes(""+id))
list.push(id);
this.saveNewCallsList(list);
},
/**
* Remove a call from the list of calls
*
* @param {Number} id The ID of the call to remove
*/
removeCallFromList: function(id){
var list = this.getCurrentCallsList();
while(list.includes(""+id))
list.splice(list.indexOf(""+id), 1);
this.saveNewCallsList(list);
},
/**
* Remove all the calls from the list
*/
removeAllCalls: function(){
this.saveNewCallsList([]);
}
}

View File

@ -0,0 +1,103 @@
/**
* Calls interface
*
* @author Pierre HUBERT
*/
ComunicWeb.components.calls.interface = {
/**
* Get calls configuration
*
* @param {function} callback Function that will be called
* once the operation has completed
*/
getConfig: function(callback){
ComunicWeb.common.api.makeAPIrequest("calls/config", {}, true, callback);
},
/**
* Create a call for a conversation
*
* @param {Number} convID The ID of the target conversation
* @param {function} callback
*/
createForConversation : function(convID, callback){
ComunicWeb.common.api.makeAPIrequest(
"calls/createForConversation",
{
conversationID: convID
},
true,
callback
);
},
/**
* Get information about a single call
*
* @param {Number} callID The ID of the target call
* @param {function} callback Function called on request result
*/
getInfo: function (callID, callback){
ComunicWeb.common.api.makeAPIrequest(
"calls/getInfo",
{
call_id: callID
},
true,
callback
);
},
/**
* Get and return the next pending call for the
* user
*
* @param {function} callback
*/
getNextPendingCall: function(callback){
ComunicWeb.common.api.makeAPIrequest(
"calls/nextPending",
{},
true,
callback
);
},
/**
* Respond to call
*
* @param {number} call_id The ID of the target call
* @param {boolean} accept TRUE to accept call / FALSE els
* @param {function} callback Function to call once response has been set
*/
respondToCall: function(call_id, accept, callback){
ComunicWeb.common.api.makeAPIrequest(
"calls/respond",
{
call_id: call_id,
accept: accept
},
true,
callback
);
},
/**
* Hang up a call
*
* @param {Number} call_id The ID of the target call
* @param {function} callback Function to call on call callback
*/
hangUp: function(call_id, callback){
ComunicWeb.common.api.makeAPIrequest(
"calls/hangUp",
{
call_id: call_id,
},
true,
callback
);
}
}

View File

@ -0,0 +1,125 @@
/**
* Calls ring screen
*
* Display a popup to ask the user whether he wants
* to respond to a call or not
*
* @author Pierre HUBERT
*/
ComunicWeb.components.calls.ringScreen = {
/**
* Song object
*
* @type {SongPlayer}
*/
_song: undefined,
/**
* Notify user about an incoming call and offer him to respond it
*
* @param {String} title The title of the conversation
* @param {number} timeout Timeout after which the call is automatically
* considered as rejected
* @param {(accept : boolean) => any} callback Callback function
* @return {Object} Information about the window
*/
show: function(title, timeout, callback){
//Initialize song first
if(this._song == undefined)
this._song = new SongPlayer([
ComunicWeb.__config.assetsURL + "audio/phone_ring.mp3",
ComunicWeb.__config.assetsURL + "audio/phone_ring.ogg"
]);
this._song.playForever();
var callContainer = createElem2({
appendTo: document.body,
type: "div",
class: "ring-call-container"
});
var callMessageBox = createElem2({
appendTo: callContainer,
type: "div",
class: "call-message-box"
});
add_p(callMessageBox, "<i>" + title + "</i> is calling you");
//Add buttons to respond to call
var respondButtons = createElem2({
appendTo: callMessageBox,
type: "div",
class: "response-buttons"
});
var rejectButton = createElem2({
appendTo: respondButtons,
type: "button",
class: "btn btn-danger",
innerHTML: "Reject"
});
var acceptButton = createElem2({
appendTo: respondButtons,
type: "button",
class: "btn btn-success",
innerHTML: "Accept"
});
var close = function(){
ComunicWeb.components.calls.ringScreen._song.stop();
//Remove elem
emptyElem(callContainer);
callContainer.remove();
}
var hasResponded = false;
var respond = function(accept){
close();
if(hasResponded)
return;
hasResponded = true;
callback(accept);
}
rejectButton.addEventListener("click", function() {
respond(false);
});
acceptButton.addEventListener("click", function(){
respond(true);
});
//Automatically reject the call after a certain amount of time
setTimeout(function(){
respond(false);
}, timeout*1000);
return {
/**
* A function to close the current ringscreen, without
* calling callback
*/
close: close,
/**
* A function to programmatically respond to call
*/
respond: respond
};
}
}

View File

@ -0,0 +1,32 @@
/**
* User media getter
*
* @author Pierre HUBERT
*/
ComunicWeb.components.calls.userMedia = {
/**
* Get user media
*
* @return {Promise} A promise to get user media
*/
get: function(){
//Use latest API
return navigator.mediaDevices
//Request user media
.getUserMedia({
audio: true,
video: true
})
//Save stream
.then(function(stream){
ComunicWeb.components.calls.userMedia._currMedia = stream;
return stream;
});
}
}

View File

@ -0,0 +1,44 @@
/**
* Calls utilities
*
* @author Pierre HUBERT
*/
ComunicWeb.components.calls.utils = {
/**
* Check out whether all the members of a conversation stop to follow it,
* except the current user
*
* @param {Object} info Information about the conversation to analyze
*/
hasEveryoneLeft: function(info){
var allDisconnected = true;
info.members.forEach(function(member){
if(member.status != "rejected" && member.status != "hang_up" && member.userID != userID())
allDisconnected = false;
});
return allDisconnected;
},
/**
* Get the current user response to a call
*
* @param {Call} call Current call information
* @return The response of the current user to the call
*/
getCurrentUserState: function(call){
var userstate = undefined;
call.members.forEach(function(member){
if(member.userID == userID())
userstate = member.status
});
return userstate;
}
};

View File

@ -56,7 +56,7 @@ ComunicWeb.components.conversations.chatWindows = {
infosBox.conversationID = infos.conversationID;
//Change box root class name
infosBox.rootElem.className += " direct-chat direct-chat-primary";
infosBox.rootElem.className += " chat-window direct-chat direct-chat-primary";
//Adapt close button behaviour
infosBox.closeFunction = function(){
@ -84,18 +84,21 @@ ComunicWeb.components.conversations.chatWindows = {
});
//Add button to get conversation members
infosBox.membersButton = createElem("button");
infosBox.closeButton.parentNode.insertBefore(infosBox.membersButton, infosBox.closeButton);
infosBox.membersButton.type = "button";
infosBox.membersButton.className = "btn btn-box-tool";
infosBox.membersButton = createElem2({
type: "button",
insertBefore: infosBox.closeButton,
elemType: "button",
class: "btn btn-box-tool",
title: "Conversation members"
});
infosBox.membersButton.setAttribute("data-toggle", "tooltip");
infosBox.membersButton.setAttribute("data-widget", "chat-pane-toggle");
infosBox.membersButton.title = "Conversation members";
//Add button icon
var buttonIcon = createElem("i", infosBox.membersButton);
buttonIcon.className = "fa fa-users";
//Add conversation members pane
var membersPane = createElem("div", infosBox.boxBody);
membersPane.className = "direct-chat-contacts";
@ -319,6 +322,9 @@ ComunicWeb.components.conversations.chatWindows = {
return false;
};
//Add call button (if possible)
ComunicWeb.components.conversations.chatWindows.showCallButton(conversationInfos);
});
});
@ -563,16 +569,46 @@ ComunicWeb.components.conversations.chatWindows = {
return true;
},
/**
* Add a call button to the conversation, if possible
*
* @param {Object} conversation Information about the conversation
*/
showCallButton: function(conversation){
//Check if calls are disabled
if(!ComunicWeb.components.calls.controller.isAvailable())
return;
//Check if it is a conversation with an exceptable number of members
if(conversation.infos.members.length < 2
|| conversation.infos.members.length > ComunicWeb.components.calls.controller.getConfig().maximum_number_members)
return;
//Add the call button
var button = createElem2({
insertBefore: conversation.box.boxTools.firstChild,
type: "button",
class: "btn btn-box-tool",
innerHTML: "<i class='fa fa-phone'></i>"
});
conversation.box.callButton = button;
button.addEventListener("click", function(){
ComunicWeb.components.calls.controller.call(conversation.infos.ID);
});
},
/**
* Process submited update conversation form
*
* @param {Object} conversation Informations about the conversation
* @param {Object} conversation Information about the conversation
* @return {Boolean} True for a success
*/
submitUpdateForm: function(conversation){
//Then, get informations about the input
//Then, get information about the input
var newValues = {
conversationID: conversation.infos.ID,
following: conversation.settingsForm.followConversationInput.checked,
@ -621,7 +657,7 @@ ComunicWeb.components.conversations.chatWindows = {
/**
* Submit a new message form
*
* @param {Object} convInfos Informations about the conversation
* @param {Object} convInfos Information about the conversation
* @return {Boolean} True for a success
*/
submitMessageForm: function(convInfos){
@ -673,7 +709,7 @@ ComunicWeb.components.conversations.chatWindows = {
/**
* Reset a create a message form
*
* @param {Object} infos Informations about the conversation
* @param {Object} infos Information about the conversation
* @return {Boolean} True for a success
*/
resetCreateMessageForm: function(infos){

View File

@ -22,6 +22,9 @@ ComunicWeb.components.conversations.messageEditor = {
function(content){
if(!content)
return;
//Intend to update message content
ComunicWeb.components.conversations.interface.UpdateSingleMessage(
message.ID,

View File

@ -54,6 +54,27 @@ ComunicWeb.components.conversations.utils = {
return true;
},
/**
* Given a conversation ID, get its name
*
* @param {number} id The ID of the target conversation
* @param {function} onGotName Function called once we have got the name of the conversation
*/
getNameForID: function(id, onGotName){
ComunicWeb.components.conversations.interface.getInfosOne(id, function(info){
//Check if an error occurred
if(info.error)
return onGotName(false);
//Get and return the name of the conversation
ComunicWeb.components.conversations.utils.getName(info, onGotName);
});
},
/**
* Create and display a conversation creation / edition form
*

View File

@ -25,14 +25,19 @@ ComunicWeb.components.notifications.interface = {
/**
* Get the number of unread news such as notifications or conversations
*
* @param {boolean} get_calls Get the number of pending calls
* @param {function} callback
*/
getAllUnread: function(callback){
getAllUnread: function(get_calls, callback){
//Perform API request
var apiURI = "notifications/count_all_news";
var params = {};
//Check if we have to get the number of pending calls
if(get_calls)
params.include_calls = true;
//Perform the request
ComunicWeb.common.api.makeAPIrequest(apiURI, params, true, callback);

View File

@ -35,7 +35,8 @@ ComunicWeb.components.notifications.service = {
//Get the number of notifications from the API
ComunicWeb.components.notifications.interface.getAllUnread(function(response){
ComunicWeb.components.notifications.interface.getAllUnread(
ComunicWeb.components.calls.controller.isAvailable(), function(response){
//Continue in case of success
if(response.error)
@ -76,6 +77,11 @@ ComunicWeb.components.notifications.service = {
ComunicWeb.components.notifications.song.play();
ComunicWeb.components.notifications.service.last_notifs_number = total_number_notifs;
//Process the number of calls if possible
if(response.calls && response.calls > 0)
ComunicWeb.components.calls.controller.newCallsAvailable(response.calls);
});
}, 2000);

View File

@ -18,24 +18,13 @@ ComunicWeb.components.notifications.song = {
//Create song element if required
if(this.songElem == null){
this.songElem = createElem2({
type: "audio"
});
createElem2({
type: "source",
appendTo: this.songElem,
src: ComunicWeb.__config.assetsURL + "audio/notif_song.mp3"
});
createElem2({
type: "source",
appendTo: this.songElem,
src: ComunicWeb.__config.assetsURL + "audio/notif_song.ogg"
});
this.songElem = new SongPlayer([
ComunicWeb.__config.assetsURL + "audio/notif_song.mp3",
ComunicWeb.__config.assetsURL + "audio/notif_song.ogg"
]);
}
//Play song
this.songElem.play();
this.songElem.playOnce();
}
}

View File

@ -424,9 +424,41 @@ ComunicWeb.components.posts.ui = {
//In case of YouTube video
else if(info.kind == "youtube"){
//Create frame placeholder
var youtube_placeholder = createElem2({
appendTo: postRoot,
type: "div",
class: "post-youtube post-youtube-placeholder"
});
//Title
createElem2({
appendTo: youtube_placeholder,
type: "div",
class: "title",
innerHTML: "<i class='fa fa-youtube-play'></i> YouTube Movie"
});
createElem2({
appendTo: youtube_placeholder,
type: "a",
class: "btn btn-default",
innerHTML: "Open on YouTube",
href: "https://youtube.com/watch?v=" + info.file_path,
}).target = "_blank";
var openHere = createElem2({
appendTo: youtube_placeholder,
type: "div",
class: "cursor-pointer",
innerHTML: "Open here"
});
openHere.addEventListener("click", function(){
//Create iframe
var youtube_iframe = createElem2({
appendTo: postRoot,
insertBefore: youtube_placeholder,
type: "iframe",
class: "post-youtube",
src: "https://www.youtube-nocookie.com/embed/"+info.file_path+"?rel=0"
@ -436,6 +468,10 @@ ComunicWeb.components.posts.ui = {
youtube_iframe.setAttribute("allow", "encrypted-media");
youtube_iframe.setAttribute("allowfullscreen", "");
youtube_placeholder.remove();
});
}
//In case of PDF

View File

@ -77,12 +77,18 @@ ComunicWeb.user.userLogin = {
//Perform next action
afterGetCurrentUserID(0);
}
else{
else
{
//Update user ID
ComunicWeb.user.userLogin.__userID = result.userID;
//Perform next action
afterGetCurrentUserID(result.userID)
afterGetCurrentUserID(result.userID);
//Notify about the event
SendEvent("got_user_id", {
userID: result.userID
});
}
};

1
assets/zip/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
personnal-data-export-navigator.zip

View File

@ -0,0 +1,9 @@
#!/bin/bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd $SCRIPT_DIR;
rm -f personnal-data-export-navigator.zip
cd personnal-data-export-navigator;
zip -r personnal-data-export-navigator.zip assets Export.html
mv personnal-data-export-navigator.zip ../

48
builder
View File

@ -63,11 +63,12 @@ function files_to_file(array $files, string $target) : bool {
/**
* Copy an array of files into a specific target file using uglifyJS
*
* @param string $begin_path The begining of each path
* @param array $files The name of the source file
* @param string $target The target file
* @return bool TRUE in case of success / FALSE in case of failure
*/
function js_files_to_file(array $files, string $target){
function js_files_to_file(string $begin_path, array $files, string $target){
$source = "";
@ -77,7 +78,28 @@ function js_files_to_file(array $files, string $target){
foreach($files as $file){
$uglifyjs = true;
//Check if file entry is an array or a string
if(is_string($file))
$file = $begin_path.$file;
//It is an array
else if(is_array($file)) {
//Check if we have special information for uglifyjs
if(isset($file["uglifyjs"]))
$uglifyjs = $file["uglifyjs"];
$file = $begin_path.$file["path"];
}
//Else the kind of entry is not supported
else
throw new Exception("Excepted string or array, got something else for javascript entry!");
//Compress file
if($uglifyjs){
notice("Parsing with UGLIFYJS: ".$file);
exec("/usr/bin/uglifyjs '".$file."' -c -o ".TEMP_FILE, $output, $exit_code);
@ -88,6 +110,14 @@ function js_files_to_file(array $files, string $target){
notice("An error (".$exit_code.") occured while parsing file ".$file, TRUE);
exit(10);
}
}
//Else we take the file as is
else
$source .= "\n".file_get_contents($file);
}
//Delete the temp file
@ -177,6 +207,7 @@ if(file_exists(OUTPUT_DIRECTORY))
mkdir(OUTPUT_DIRECTORY, 0777, true);
mkdir($path_release_assets, 0777, true);
mkdir($path_release_assets."/css", 0777, true);
mkdir($path_release_assets."/zip", 0777, true);
@ -191,9 +222,8 @@ files_to_file($thirdPartyDebugFiles, $targetThirdPartyCSS);
//3rd party JS
notice("Third Party JS");
$thirdPartyDebugFiles = array_put_begining($path_debug_assets, $debug::THIRD_PARTY_JS);
$targetThirdPartyJS = $path_release_assets.$release::THIRD_PARTY_JS;
js_files_to_file($thirdPartyDebugFiles, $targetThirdPartyJS);
js_files_to_file($path_debug_assets, $debug::THIRD_PARTY_JS, $targetThirdPartyJS);
//App CSS
notice("App CSS");
@ -203,9 +233,8 @@ files_to_file($appDebugFiles, $targetAppCSS);
//App JS
notice("App JS");
$appDebugFiles = array_put_begining($path_debug_assets, $debug::APP_JS);
$targetAppJS = $path_release_assets.$release::APP_JS;
js_files_to_file($appDebugFiles, $targetAppJS);
js_files_to_file($path_debug_assets, $debug::APP_JS, $targetAppJS);
//Make some adpations on third party files
@ -230,6 +259,8 @@ rcopy($path_debug_assets."3rdparty/adminLTE/plugins/iCheck/flat/icheck-flat-imgs
rcopy($path_debug_assets."img/", $path_release_assets."img/");
rcopy($path_debug_assets."templates/", $path_release_assets."templates/");
//Copy songs
rcopy($path_debug_assets."audio/", $path_release_assets."audio/");
//Copy dark theme
rcopy($path_debug_assets."css/dark_theme.css", $path_release_assets."css/dark_theme.css");
@ -237,6 +268,13 @@ rcopy($path_debug_assets."css/dark_theme.css", $path_release_assets."css/dark_th
//Copy pacman
rcopy($path_debug_assets."3rdparty/pacman", $path_release_assets."3rdparty/pacman");
//Build and copy personnal data navigator
notice("Build personnal data export navigator and add it to built files");
exec($path_debug_assets."zip/personnal-data-export-navigator-builder.sh");
rcopy($path_debug_assets."zip/personnal-data-export-navigator.zip", $path_release_assets."zip/personnal-data-export-navigator.zip");
//Begin to write root PHP File
notice("Generate PHP root file");
$page_src = '<?php

View File

@ -159,6 +159,10 @@ class Dev {
//JS BBCode Parser
"3rdparty/js-bbcode-parser/bbcode-config.js",
"3rdparty/js-bbcode-parser/bbcode-parser.js",
//Simple peer
array("path" => "3rdparty/simplepeer/simplepeer.min.js", "uglifyjs" => false),
array("path" => "3rdparty/SignalExchangerClient/SignalExchangerClient.js", "uglifyjs" => false)
);
/**
@ -218,6 +222,10 @@ class Dev {
//Incognito mode component
"css/components/incognito/ui.css",
//Calls component
"css/components/calls/callWindow.css",
"css/components/calls/ringScreen.css",
//Pacman (easter egg) stylesheet
"css/components/pacman.css",
@ -312,6 +320,7 @@ class Dev {
"js/common/formChecker.js",
"js/common/date.js",
"js/common/system.js",
array("path" => "js/common/songPlayer.js", "uglifyjs" => false),
//Languages
"js/langs/en.inc.js",
@ -437,6 +446,15 @@ class Dev {
"js/components/incognito/management.js",
"js/components/incognito/keyboard.js",
//Calls compontent
"js/components/calls/interface.js",
"js/components/calls/controller.js",
"js/components/calls/callWindow.js",
"js/components/calls/currentList.js",
"js/components/calls/userMedia.js",
"js/components/calls/ringScreen.js",
"js/components/calls/utils.js",
//Pacman component (easter egg)
"js/components/pacman.js",

View File

@ -134,6 +134,10 @@ function src_inc_list_js(string $assets_url, array $files) : string {
//Process the list of files
foreach($files as $file){
if(is_array($file))
$file = $file["path"];
$source .= src_inc_js($assets_url.$file)."\n\t\t";
}

View File

@ -2,7 +2,7 @@
Comunic web app client
Main HTML file
(c) Pierre HUBERT 2017-2018
(c) Pierre HUBERT 2017-2019
-->
<!DOCTYPE html>
<html>