47 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
34 changed files with 2480 additions and 58 deletions

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) - SCEditor (BBC WYIWYG editor) (https://github.com/samclarke/SCEditor) (MIT License)
- JavaScript BBCode Parser (https://github.com/Frug/js-bbcode-parser) (MIT License) - JavaScript BBCode Parser (https://github.com/Frug/js-bbcode-parser) (MIT License)
- Pacman (https://github.com/daleharvey/pacman) (WTFPL 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; color: #72afd2;
} }
.cursor-pointer {
cursor: pointer;
}
/** /**
* Sceditor iframe * 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 * @author Pierre HUBERT
*/ */
/**
* Main definition
*/
#conversationsElem .box { #conversationsElem .box {
width: 250px; width: 250px;
height: 350px; height: 350px;
@ -15,6 +19,10 @@
margin-bottom: 0px; margin-bottom: 0px;
} }
#conversationsElem .chat-window .box-title {
font-size: 15px !important;
}
/** /**
* Conversations create message form * Conversations create message form
*/ */

View File

@ -63,6 +63,20 @@
text-align: center; 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 { .post .post-youtube {
width: 100%; width: 100%;
height: 300px; height: 300px;

View File

@ -354,11 +354,16 @@ div.sceditor-dropdown input {
color: var(--black5); color: var(--black5);
} }
.select2-dropdown { .select2-dropdown, .select2-selection,
background-color: var(--black5); .select2-search__field {
background-color: var(--black5) !important;
color: var(--white); color: var(--white);
} }
.select2-selection__rendered {
color: var(--white) !important;
}
.select2-container--default .select2-results__option--highlighted[aria-selected] { .select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: var(--black4); background-color: var(--black4);
color: var(--white); color: var(--white);
@ -445,6 +450,10 @@ div.sceditor-dropdown input {
filter: brightness(100%); filter: brightness(100%);
} }
.post-youtube-placeholder {
background-color: var(--black6) !important;
}
.box-comments .username, .box-comments .username,
.box-comments .comment-content { .box-comments .comment-content {
color: var(--white); color: var(--white);
@ -572,3 +581,46 @@ img[src$="groups_logo/default.png"] {
.page_account_created .message_container .message { .page_account_created .message_container .message {
background-color: var(--black5); 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 * @return {String} The generated date
*/ */
diffToStr: function(difference){ diffToStr: function(difference){
//Check if difference is less than one second
if(difference < 0)
difference = 0;
//Calculate seconds //Calculate seconds
var seconds = difference-Math.floor(difference/60)*60; var seconds = difference-Math.floor(difference/60)*60;
var difference = (difference - seconds)/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 * 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(); ComunicWeb.components.darkTheme.refresh();
/**
* Initialize call system
*/
ComunicWeb.components.calls.controller.init();
/** /**
* What to do after login refresh * What to do after login refresh
*/ */

View File

@ -701,3 +701,56 @@ function SendEvent(name, details){
document.dispatchEvent(event); 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

@ -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; infosBox.conversationID = infos.conversationID;
//Change box root class name //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 //Adapt close button behaviour
infosBox.closeFunction = function(){ infosBox.closeFunction = function(){
@ -84,18 +84,21 @@ ComunicWeb.components.conversations.chatWindows = {
}); });
//Add button to get conversation members //Add button to get conversation members
infosBox.membersButton = createElem("button"); infosBox.membersButton = createElem2({
infosBox.closeButton.parentNode.insertBefore(infosBox.membersButton, infosBox.closeButton); type: "button",
infosBox.membersButton.type = "button"; insertBefore: infosBox.closeButton,
infosBox.membersButton.className = "btn btn-box-tool"; elemType: "button",
class: "btn btn-box-tool",
title: "Conversation members"
});
infosBox.membersButton.setAttribute("data-toggle", "tooltip"); infosBox.membersButton.setAttribute("data-toggle", "tooltip");
infosBox.membersButton.setAttribute("data-widget", "chat-pane-toggle"); infosBox.membersButton.setAttribute("data-widget", "chat-pane-toggle");
infosBox.membersButton.title = "Conversation members";
//Add button icon //Add button icon
var buttonIcon = createElem("i", infosBox.membersButton); var buttonIcon = createElem("i", infosBox.membersButton);
buttonIcon.className = "fa fa-users"; buttonIcon.className = "fa fa-users";
//Add conversation members pane //Add conversation members pane
var membersPane = createElem("div", infosBox.boxBody); var membersPane = createElem("div", infosBox.boxBody);
membersPane.className = "direct-chat-contacts"; membersPane.className = "direct-chat-contacts";
@ -319,6 +322,9 @@ ComunicWeb.components.conversations.chatWindows = {
return false; return false;
}; };
//Add call button (if possible)
ComunicWeb.components.conversations.chatWindows.showCallButton(conversationInfos);
}); });
}); });
@ -563,16 +569,46 @@ ComunicWeb.components.conversations.chatWindows = {
return true; 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 * 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 * @return {Boolean} True for a success
*/ */
submitUpdateForm: function(conversation){ submitUpdateForm: function(conversation){
//Then, get informations about the input //Then, get information about the input
var newValues = { var newValues = {
conversationID: conversation.infos.ID, conversationID: conversation.infos.ID,
following: conversation.settingsForm.followConversationInput.checked, following: conversation.settingsForm.followConversationInput.checked,
@ -621,7 +657,7 @@ ComunicWeb.components.conversations.chatWindows = {
/** /**
* Submit a new message form * 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 * @return {Boolean} True for a success
*/ */
submitMessageForm: function(convInfos){ submitMessageForm: function(convInfos){
@ -673,7 +709,7 @@ ComunicWeb.components.conversations.chatWindows = {
/** /**
* Reset a create a message form * 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 * @return {Boolean} True for a success
*/ */
resetCreateMessageForm: function(infos){ resetCreateMessageForm: function(infos){

View File

@ -54,6 +54,27 @@ ComunicWeb.components.conversations.utils = {
return true; 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 * 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 * Get the number of unread news such as notifications or conversations
* *
* @param {boolean} get_calls Get the number of pending calls
* @param {function} callback * @param {function} callback
*/ */
getAllUnread: function(callback){ getAllUnread: function(get_calls, callback){
//Perform API request //Perform API request
var apiURI = "notifications/count_all_news"; var apiURI = "notifications/count_all_news";
var params = {}; var params = {};
//Check if we have to get the number of pending calls
if(get_calls)
params.include_calls = true;
//Perform the request //Perform the request
ComunicWeb.common.api.makeAPIrequest(apiURI, params, true, callback); 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 //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 //Continue in case of success
if(response.error) if(response.error)
@ -76,6 +77,11 @@ ComunicWeb.components.notifications.service = {
ComunicWeb.components.notifications.song.play(); ComunicWeb.components.notifications.song.play();
ComunicWeb.components.notifications.service.last_notifs_number = total_number_notifs; 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); }, 2000);

View File

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

View File

@ -424,9 +424,41 @@ ComunicWeb.components.posts.ui = {
//In case of YouTube video //In case of YouTube video
else if(info.kind == "youtube"){ 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 //Create iframe
var youtube_iframe = createElem2({ var youtube_iframe = createElem2({
appendTo: postRoot, insertBefore: youtube_placeholder,
type: "iframe", type: "iframe",
class: "post-youtube", class: "post-youtube",
src: "https://www.youtube-nocookie.com/embed/"+info.file_path+"?rel=0" 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("allow", "encrypted-media");
youtube_iframe.setAttribute("allowfullscreen", ""); youtube_iframe.setAttribute("allowfullscreen", "");
youtube_placeholder.remove();
});
} }
//In case of PDF //In case of PDF

View File

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

40
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 * 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 array $files The name of the source file
* @param string $target The target file * @param string $target The target file
* @return bool TRUE in case of success / FALSE in case of failure * @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 = ""; $source = "";
@ -77,7 +78,28 @@ function js_files_to_file(array $files, string $target){
foreach($files as $file){ 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 //Compress file
if($uglifyjs){
notice("Parsing with UGLIFYJS: ".$file); notice("Parsing with UGLIFYJS: ".$file);
exec("/usr/bin/uglifyjs '".$file."' -c -o ".TEMP_FILE, $output, $exit_code); 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); notice("An error (".$exit_code.") occured while parsing file ".$file, TRUE);
exit(10); exit(10);
} }
}
//Else we take the file as is
else
$source .= "\n".file_get_contents($file);
} }
//Delete the temp file //Delete the temp file
@ -192,9 +222,8 @@ files_to_file($thirdPartyDebugFiles, $targetThirdPartyCSS);
//3rd party JS //3rd party JS
notice("Third 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; $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 //App CSS
notice("App CSS"); notice("App CSS");
@ -204,9 +233,8 @@ files_to_file($appDebugFiles, $targetAppCSS);
//App JS //App JS
notice("App JS"); notice("App JS");
$appDebugFiles = array_put_begining($path_debug_assets, $debug::APP_JS);
$targetAppJS = $path_release_assets.$release::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 //Make some adpations on third party files
@ -231,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."img/", $path_release_assets."img/");
rcopy($path_debug_assets."templates/", $path_release_assets."templates/"); rcopy($path_debug_assets."templates/", $path_release_assets."templates/");
//Copy songs
rcopy($path_debug_assets."audio/", $path_release_assets."audio/");
//Copy dark theme //Copy dark theme
rcopy($path_debug_assets."css/dark_theme.css", $path_release_assets."css/dark_theme.css"); rcopy($path_debug_assets."css/dark_theme.css", $path_release_assets."css/dark_theme.css");

View File

@ -159,6 +159,10 @@ class Dev {
//JS BBCode Parser //JS BBCode Parser
"3rdparty/js-bbcode-parser/bbcode-config.js", "3rdparty/js-bbcode-parser/bbcode-config.js",
"3rdparty/js-bbcode-parser/bbcode-parser.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 //Incognito mode component
"css/components/incognito/ui.css", "css/components/incognito/ui.css",
//Calls component
"css/components/calls/callWindow.css",
"css/components/calls/ringScreen.css",
//Pacman (easter egg) stylesheet //Pacman (easter egg) stylesheet
"css/components/pacman.css", "css/components/pacman.css",
@ -312,6 +320,7 @@ class Dev {
"js/common/formChecker.js", "js/common/formChecker.js",
"js/common/date.js", "js/common/date.js",
"js/common/system.js", "js/common/system.js",
array("path" => "js/common/songPlayer.js", "uglifyjs" => false),
//Languages //Languages
"js/langs/en.inc.js", "js/langs/en.inc.js",
@ -437,6 +446,15 @@ class Dev {
"js/components/incognito/management.js", "js/components/incognito/management.js",
"js/components/incognito/keyboard.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) //Pacman component (easter egg)
"js/components/pacman.js", "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 //Process the list of files
foreach($files as $file){ foreach($files as $file){
if(is_array($file))
$file = $file["path"];
$source .= src_inc_js($assets_url.$file)."\n\t\t"; $source .= src_inc_js($assets_url.$file)."\n\t\t";
} }