This adds a person selector as well as avatars so that initiating a call will be more intuitive.

Fixes https://github.com/nextcloud/spreed/issues/25
Fixes https://github.com/nextcloud/spreed/issues/11

Signed-off-by: Lukas Reschke <lukas@statuscode.ch>
This commit is contained in:
Lukas Reschke 2016-10-19 12:00:17 +02:00
Родитель b36776e78a
Коммит 519c862d28
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B9F6980CF6E759B1
12 изменённых файлов: 385 добавлений и 171 удалений

Просмотреть файл

@ -50,15 +50,13 @@
<notnull>true</notnull>
<length>255</length>
</field>
<index>
<name>spreedme_rooms_name_index</name>
<unique>true</unique>
<field>
<name>name</name>
<sorting>ascending</sorting>
</field>
</index>
<field>
<name>type</name>
<type>integer</type>
<default>0</default>
<notnull>true</notnull>
<length>4</length>
</field>
</declaration>
</table>
<table>
@ -83,13 +81,17 @@
</field>
<index>
<name>spreedme_room_participants_userId_index</name>
<name>spreedme_room_participants_userid_roomid</name>
<primary>true</primary>
<unique>true</unique>
<field>
<name>userId</name>
<sorting>ascending</sorting>
</field>
<field>
<name>roomId</name>
<sorting>ascending</sorting>
</field>
</index>
</declaration>
</table>

Просмотреть файл

@ -6,7 +6,7 @@
<licence>AGPLv3+</licence>
<author>Lukas Reschke, Jan-Christoph Borchardt</author>
<default_enable/>
<version>1.0.7</version>
<version>1.0.14</version>
<dependencies>
<owncloud min-version="9.1" max-version="9.2" />
</dependencies>

Просмотреть файл

@ -48,11 +48,6 @@ return [
'url' => '/api/room',
'verb' => 'GET',
],
[
'name' => 'api#joinRoom',
'url' => '/api/room/{roomId}/join',
'verb' => 'POST',
],
[
'name' => 'api#getPeersInRoom',
'url' => '/api/room/{roomId}/peers',
@ -67,6 +62,12 @@ return [
'name' => 'AppSettings#setSpreedSettings',
'url' => '/settings',
'verb' => 'POST',
]
],
[
'name' => 'api#createOneToOneVideoCallRoom',
'url' => '/api/oneToOne',
'verb' => 'PUT',
],
],
];

Просмотреть файл

@ -9,12 +9,44 @@
top: 0;
border-bottom: 1px solid #eee;
}
#oca-spreedme-add-room input {
/**
* Sidebar styles
*/
#oca-spreedme-add-room .select2-container {
width: 100%;
padding: 14px 44px 14px 12px;
margin: 0;
border: none;
}
#oca-spreedme-add-room .select2-arrow {
display: none;
}
#oca-spreedme-add-room .select2-default {
border: none;
border-radius: 0;
background-image: none;
}
.select2-chosen {
padding-top: 2px;
}
.select2-chosen .avatar {
float: left;
margin-right: 5px;
}
#select2-drop .avatar {
float: left;
margin-right: 5px;
}
.select2-result-label {
height: 35px;
}
.select2-choice {
height: 35px !important;
}
#app-navigation .avatar {
float: left;
}
#oca-spreedme-add-room button {
position: absolute;
background-color: transparent;

101
js/app.js
Просмотреть файл

@ -33,37 +33,78 @@
_registerPageEvents: function() {
var self = this;
$('#oca-spreedme-add-room').submit(function() {
return false;
$('#edit-roomname').select2({
ajax: {
url: OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees',
dataType: 'json',
quietMillis: 100,
data: function (term) {
return {
format: 'json',
search: term,
perPage: 200,
itemType: 'folder',
shareType: 0
};
},
results: function (response) {
// TODO improve error case
if (response.ocs.data === undefined) {
console.error('Failure happened', response);
return;
}
var results = [];
$.each(response.ocs.data.users, function(id, user) {
results.push({ id: user.value.shareWith});
});
return {
results: results,
more: false
};
}
},
initSelection: function (element, callback) {
console.log(element);
callback({id: element.val()});
},
formatResult: function (element) {
return '<span><div class="avatar" data-user="' + escapeHTML(element.id) + '" data-user-display-name="' + escapeHTML(element.id) + '"></div>' + escapeHTML(element.id) + '</span>';
},
formatSelection: function (element) {
return '<span><div class="avatar" data-user="' + escapeHTML(element.id) + '" data-user-display-name="' + escapeHTML(element.id) + '"></div>' + escapeHTML(element.id) + '</span>';
}
});
$('#edit-roomname').on("change", function(e) {
OCA.SpreedMe.Rooms.createOneToOneVideoCall(e.val);
$('body').find('.avatar').each(function () {
var element = $(this);
if (element.data('user-display-name')) {
element.avatar(element.data('user'), 28, undefined, false, undefined, element.data('user-display-name'));
} else {
element.avatar(element.data('user'), 28);
}
});
});
$('#edit-roomname').on("click", function() {
$('body').find('.avatar').each(function () {
var element = $(this);
if (element.data('user-display-name')) {
element.avatar(element.data('user'), 28, undefined, false, undefined, element.data('user-display-name'));
} else {
element.avatar(element.data('user'), 28);
}
});
});
// Create a new room
$('#oca-spreedme-add-room > button.icon-confirm').click(function() {
var roomname = $('#oca-spreedme-add-room > input[type="text"]').val();
if (roomname === "") {
return;
}
self._rooms.create({
name: roomname
}, {
success: function(data) {
OCA.SpreedMe.Rooms.join(data.get('id'));
}, error: function(jqXHR, status, error) {
var message;
var editRoomname = $('#edit-roomname');
try {
message = JSON.parse(jqXHR.responseText).message;
} catch (e) {
// Ignore exception, received no/invalid JSON.
}
if (!message) {
message = jqXHR.responseText || error;
}
editRoomname.prop('title', message);
editRoomname.tooltip({placement: 'right', trigger: 'manual'});
editRoomname.tooltip('show');
editRoomname.addClass('error');
$('#edit-roomname').on("select2-loaded", function() {
$('body').find('.avatar').each(function () {
var element = $(this);
if (element.data('user-display-name')) {
element.avatar(element.data('user'), 28, undefined, false, undefined, element.data('user-display-name'));
} else {
element.avatar(element.data('user'), 28);
}
});
});
@ -131,7 +172,7 @@
OCA.SpreedMe.Rooms.join(window.location.hash.substring(1));
});
if (window.location.hash.substring(1) === '') {
OCA.SpreedMe.Rooms.join();
OCA.SpreedMe.Rooms.showCamera();
}
},
_showRoomList: function() {

Просмотреть файл

@ -9,42 +9,58 @@
function initRooms() {
var editRoomname = $('#edit-roomname');
editRoomname.keyup(function() {
editRoomname.keyup(function () {
editRoomname.tooltip('hide');
editRoomname.removeClass('error');
});
}
var currentRoomId = 0;
var currentRoomId = 0;
var roomChannel = Backbone.Radio.channel('rooms');
OCA.SpreedMe.Rooms = {
join: function(roomId) {
$('#emptycontent').hide();
$('.videoView').addClass('hidden');
$('#app-content').addClass('icon-loading');
OCA.SpreedMe.Rooms = {
showCamera: function() {
$('.videoView').removeClass('hidden');
},
createOneToOneVideoCall: function(recipientUserId) {
console.log(recipientUserId);
$.ajax({
url: OC.generateUrl('/apps/spreed/api/oneToOne'),
type: 'PUT',
data: 'targetUserName='+recipientUserId,
success: function(data) {
OCA.SpreedMe.Rooms.join(data.roomId);
}
});
},
join: function(roomId) {
$('#emptycontent').hide();
$('.videoView').addClass('hidden');
$('#app-content').addClass('icon-loading');
$('li[data-id="'+roomId+'"]').addClass('active');
currentRoomId = roomId;
OCA.SpreedMe.webrtc.joinRoom(roomId);
currentRoomId = roomId;
OCA.SpreedMe.webrtc.joinRoom(roomId);
roomChannel.trigger('active', roomId);
OCA.SpreedMe.Rooms.ping();
},
currentRoom: function() {
return currentRoomId;
},
peers: function(roomId) {
return $.ajax({
url: OC.generateUrl('/apps/spreed/api/room/{roomId}/peers', {roomId: roomId})
});
},
ping: function() {
$.post(
OC.generateUrl('/apps/spreed/api/ping'),
{
currentRoom: OCA.SpreedMe.Rooms.currentRoom()
}
);
}
};
OCA.SpreedMe.Rooms.ping();
},
currentRoom: function() {
return currentRoomId;
},
peers: function(roomId) {
return $.ajax({
url: OC.generateUrl('/apps/spreed/api/room/{roomId}/peers', {roomId: roomId})
});
},
ping: function() {
$.post(
OC.generateUrl('/apps/spreed/api/ping'),
{
currentRoom: OCA.SpreedMe.Rooms.currentRoom()
}
);
}
};
$(document).click(function(e) {
var target = e.target;

Просмотреть файл

@ -26,7 +26,7 @@
OCA.SpreedMe = OCA.SpreedMe || {};
OCA.SpreedMe.Views = OCA.SpreedMe.Views || {};
var ITEM_TEMPLATE = '<a href="#{{id}}">{{name}}</a>'+
var ITEM_TEMPLATE = '<a href="#{{id}}"><div class="avatar" data-userName="{{name}}"></div> {{name}}</a>'+
'<span class="utils">'+
'<span class="action">{{count}}</span>'+
'<span class="action icon-more" href="#" title="More" role="button"></span>'+
@ -62,6 +62,10 @@
} else {
this.$el.removeClass('active');
}
_.each(this.$el.find('.avatar'), function(a) {
$(a).avatar($(a).data('username'), 32);
});
},
template: Handlebars.compile(ITEM_TEMPLATE)
});

Просмотреть файл

@ -23,13 +23,17 @@
namespace OCA\Spreed\Controller;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use OCA\Spreed\Exceptions\RoomNotFoundException;
use OCA\Spreed\Room;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Security\ISecureRandom;
class ApiController extends Controller {
/** @var string */
@ -38,22 +42,33 @@ class ApiController extends Controller {
private $dbConnection;
/** @var IL10N */
private $l10n;
/** @var IUserManager */
private $userManager;
/** @var ISecureRandom */
private $secureRandom;
/**
* @param string $appName
* @param string $UserId
* @param IRequest $request
* @param IDBConnection $dbConnection
* @param IL10N $l10n
* @param IUserManager $userManager
* @param ISecureRandom $secureRandom
*/
public function __construct($appName,
$UserId,
IRequest $request,
IDBConnection $dbConnection,
IL10N $l10n) {
IL10N $l10n,
IUserManager $userManager,
ISecureRandom $secureRandom) {
parent::__construct($appName, $request);
$this->userId = $UserId;
$this->dbConnection = $dbConnection;
$this->l10n = $l10n;
$this->userManager = $userManager;
$this->secureRandom = $secureRandom;
}
/**
@ -71,118 +86,83 @@ class ApiController extends Controller {
}
/**
* Get all participants for a room
*
* @param int $roomId
* @return array
*/
private function getAllRooms() {
private function getRoomParticipants($roomId) {
$qb = $this->dbConnection->getQueryBuilder();
return $qb->select('*')
->from('spreedme_rooms')
->from('spreedme_room_participants')
->where($qb->expr()->eq('roomId', $qb->createNamedParameter($roomId)))
->execute()
->fetchAll();
}
/**
* @param int $roomId
*/
private function deleteRoom($roomId) {
$qb = $this->dbConnection->getQueryBuilder();
$qb->delete('spreedme_rooms')
->where($qb->expr()->eq('id', $qb->createNamedParameter($roomId)))
->execute();
}
/**
* Checks for all empty rooms and deletes them
*/
private function deleteEmptyRooms() {
$rooms = $this->getAllRooms();
foreach($rooms as $room) {
$activePeers = $this->getActivePeers($room['id']);
if(count($activePeers) === 0) {
//$this->deleteRoom($room['id']);
}
}
}
/**
* Get all currently existent rooms
* Get all currently existent rooms which the user has joined
*
* @NoAdminRequired
* @NoCSRFRequired
*
* @throws \Exception
* @return JSONResponse
*/
public function getRooms() {
$this->deleteEmptyRooms();
$qb = $this->dbConnection->getQueryBuilder();
$rooms = $qb->select('*')
->from('spreedme_rooms')
->from('spreedme_rooms', 'r')
->leftJoin('r', 'spreedme_room_participants', 'p', $qb->expr()->andX(
$qb->expr()->eq('p.userId', $qb->createNamedParameter($this->userId)),
$qb->expr()->eq('p.roomId', 'r.id')
))
->where($qb->expr()->isNotNull('p.userId'))
->execute()
->fetchAll();
foreach($rooms as $key => $room) {
$validRoom = false;
switch($room['type']) {
case Room::ONE_TO_ONE_CALL:
// As name of the room use the name of the other person participating
$participantsInCall = $this->getRoomParticipants($room['id']);
switch(count($participantsInCall)) {
case 1:
// Empty call, this means the other person has left
// the room. For now ignore this situation
$validRoom = false;
continue;
case 2:
// Two people are in the room. This is expected, now
// read out the other recipient in the room.
foreach($participantsInCall as $participant) {
if($participant['userId'] !== $this->userId) {
$rooms[$key]['name'] = $participant['userId'];
}
}
$validRoom = true;
break;
default:
$validRoom = false;
// TODO: This should not really ever happen. Add some
// error handling and fail here.
}
break;
default:
// TODO: More sane handling and logging of the room. Because
// This shouldn't happen.
continue;
}
$rooms[$key]['validRoom'] = $validRoom;
$rooms[$key]['count'] = count($this->getActivePeers($room['id']));
}
return new JSONResponse($rooms);
}
/**
* @NoAdminRequired
*
* @param string $name
* @return JSONResponse
*/
public function createRoom($name) {
$query = $this->dbConnection->getQueryBuilder();
$query->insert('spreedme_rooms')
->values(
[
'name' => $query->createNamedParameter($name),
]
);
try {
$query->execute();
} catch (UniqueConstraintViolationException $e) {
return new JSONResponse(
[
'message' => $this->l10n->t('A room with this name already exists.'),
], Http::STATUS_CONFLICT
);
}
return new JSONResponse([
'id' => $query->getLastInsertId(),
]);
}
/**
* @NoAdminRequired
*
* @param int $roomId
* @return JSONResponse
*/
public function joinRoom($roomId) {
$qb = $this->dbConnection->getQueryBuilder();
// Remove from any current room that the participant is in
$qb->delete('spreedme_room_participants')
->where($qb->expr()->eq('userId', $qb->createNamedParameter($this->userId)))
->execute();
// Add to new room
$qb->insert('spreedme_room_participants')
->values(
[
'userId' => $qb->createNamedParameter($this->userId),
'roomId' => $qb->createNamedParameter($roomId),
'lastPing' => $qb->createNamedParameter(time()),
]
)
->execute();
return new JSONResponse();
}
/**
* @NoAdminRequired
*
@ -193,6 +173,91 @@ class ApiController extends Controller {
return new JSONResponse($this->getActivePeers($roomId));
}
/**
* Returns the private chat room for two users or if not existent a
* RoomNotFoundException
*
* @param string $user1
* @param string $user2
* @return int
* @throws RoomNotFoundException
*/
private function getPrivateChatRoomForUsers($user1, $user2) {
$qb = $this->dbConnection->getQueryBuilder();
$results = $qb->select('*')
->from('spreedme_rooms', 'r1')
->leftJoin('r1', 'spreedme_room_participants', 'p1', $qb->expr()->andX(
$qb->expr()->eq('p1.userId', $qb->createNamedParameter($user1)),
$qb->expr()->eq('p1.roomId', 'r1.id')
))
->where($qb->expr()->isNotNull('p2.userId'))
->leftJoin('r1', 'spreedme_room_participants', 'p2', $qb->expr()->andX(
$qb->expr()->eq('p2.userId', $qb->createNamedParameter($user2)),
$qb->expr()->eq('p2.roomId', 'r1.id')
))
->execute()
->fetchAll();
if(count($results) > 1) {
return (int)$results[(count($results-1))]['roomId'];
}
throw new RoomNotFoundException();
}
/**
* Initiates a one-to-one video call from the urrent user to the recipient
*
* @NoAdminRequired
*
* @param string $targetUserName
* @return JSONResponse
*/
public function createOneToOneVideoCallRoom($targetUserName) {
// Get the user
$targetUser = $this->userManager->get($targetUserName);
if(!($targetUser instanceof IUser)) {
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}
// If room exists: Reuse that one, otherwise create a new one.
try {
$roomId = $this->getPrivateChatRoomForUsers($targetUser->getUID(), $this->userId);
return new JSONResponse(['roomId' => $roomId], Http::STATUS_CONFLICT);
} catch (RoomNotFoundException $e) {
// Create the room
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert('spreedme_rooms')
->values(
[
'name' => $qb->createNamedParameter($this->secureRandom->generate(12)),
'type' => $qb->createNamedParameter(Room::ONE_TO_ONE_CALL),
]
)
->execute();
$roomId = $qb->getLastInsertId();
// Add both users to new room
$usersToAdd = [
$targetUser->getUID(),
$this->userId,
];
foreach($usersToAdd as $user) {
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert('spreedme_room_participants')
->values(
[
'userId' => $qb->createNamedParameter($user),
'roomId' => $qb->createNamedParameter($roomId),
'lastPing' => $qb->createNamedParameter('0'),
]
)
->execute();
}
return new JSONResponse(['roomId' => $roomId], Http::STATUS_CREATED);
}
}
/**
* @NoAdminRequired
*

Просмотреть файл

@ -62,9 +62,6 @@ class PageController extends Controller {
$qb->delete('spreedme_messages')
->where($qb->expr()->eq('recipient', $qb->createNamedParameter($this->userId)))
->execute();
$qb->delete('spreedme_room_participants')
->where($qb->expr()->eq('userId', $qb->createNamedParameter($this->userId)))
->execute();
$params = [
'sessionId' => $this->userId,

Просмотреть файл

@ -0,0 +1,27 @@
<?php
/**
* @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Spreed\Exceptions;
class RoomNotFoundException extends \Exception {
}

26
lib/Room.php Normal file
Просмотреть файл

@ -0,0 +1,26 @@
<?php
/**
* @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Spreed;
class Room {
const ONE_TO_ONE_CALL = 1;
}

Просмотреть файл

@ -2,6 +2,9 @@
/** @var \OCP\IL10N $l */
/** @var array $_ */
vendor_script('select2/select2');
vendor_style('select2/select2');
style('spreed', 'style');
script(
'spreed',
@ -24,8 +27,8 @@ script(
<div id="app" data-sessionId="<?php p($_['sessionId']) ?>">
<div id="app-navigation" class="icon-loading">
<form id="oca-spreedme-add-room">
<input id="edit-roomname" type="text" placeholder="<?php p($l->t('Choose room name …')) ?>"/>
<button class="icon-confirm" title="<?php p($l->t('Create new room')) ?>"></button>
<input id="edit-roomname" type="text" placeholder="<?php p($l->t('Choose person …')) ?>"/>
<!-- <button class="icon-confirm" title="<?php p($l->t('Create new room')) ?>"></button> -->
</form>
<ul id="spreedme-room-list">
</ul>