mirror of https://github.com/sualko/cloud_bbb
Merge branch 'SpechtD-feat-clone-room'
commit
6a41825856
|
@ -8,6 +8,8 @@ return [
|
||||||
],
|
],
|
||||||
'routes' => [
|
'routes' => [
|
||||||
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
||||||
|
['name' => 'server#isRunning', 'url' => '/server/{roomUid}/isRunning', 'verb' => 'GET'],
|
||||||
|
['name' => 'server#insertDocument', 'url' => '/server/{roomUid}/insertDocument', 'verb' => 'POST'],
|
||||||
['name' => 'server#records', 'url' => '/server/{roomUid}/records', 'verb' => 'GET'],
|
['name' => 'server#records', 'url' => '/server/{roomUid}/records', 'verb' => 'GET'],
|
||||||
['name' => 'server#check', 'url' => '/server/check', 'verb' => 'POST'],
|
['name' => 'server#check', 'url' => '/server/check', 'verb' => 'POST'],
|
||||||
['name' => 'server#version', 'url' => '/server/version', 'verb' => 'GET'],
|
['name' => 'server#version', 'url' => '/server/version', 'verb' => 'GET'],
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\BigBlueButton\BackgroundJob;
|
||||||
|
|
||||||
|
use OCA\BigBlueButton\BigBlueButton\API;
|
||||||
|
use OCA\BigBlueButton\Service\RoomNotFound;
|
||||||
|
use OCA\BigBlueButton\Service\RoomService;
|
||||||
|
use OCP\AppFramework\Utility\ITimeFactory;
|
||||||
|
use OCP\BackgroundJob\IJobList;
|
||||||
|
use OCP\BackgroundJob\TimedJob;
|
||||||
|
|
||||||
|
class IsRunningJob extends TimedJob {
|
||||||
|
|
||||||
|
/** @var IJobList */
|
||||||
|
private $jobList;
|
||||||
|
|
||||||
|
/** @var RoomService */
|
||||||
|
private $service;
|
||||||
|
|
||||||
|
/** @var API */
|
||||||
|
private $api;
|
||||||
|
|
||||||
|
public function __construct(ITimeFactory $time, IJobList $jobList, RoomService $service, API $api) {
|
||||||
|
parent::__construct($time);
|
||||||
|
|
||||||
|
$this->jobList = $jobList;
|
||||||
|
$this->service = $service;
|
||||||
|
$this->api = $api;
|
||||||
|
|
||||||
|
$this->setInterval(15 * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function run($argument) {
|
||||||
|
try {
|
||||||
|
$room = $this->service->find($argument['id']);
|
||||||
|
} catch (RoomNotFound $e) {
|
||||||
|
$this->jobList->remove($this, $argument);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$room->running) {
|
||||||
|
$this->jobList->remove($this, $argument);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isRunning = $this->api->isRunning($room);
|
||||||
|
|
||||||
|
if (!$isRunning) {
|
||||||
|
$this->service->updateRunning($room->id, $isRunning);
|
||||||
|
|
||||||
|
$this->jobList->remove($this, $argument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ use BigBlueButton\Core\Record;
|
||||||
use BigBlueButton\Parameters\CreateMeetingParameters;
|
use BigBlueButton\Parameters\CreateMeetingParameters;
|
||||||
use BigBlueButton\Parameters\DeleteRecordingsParameters;
|
use BigBlueButton\Parameters\DeleteRecordingsParameters;
|
||||||
use BigBlueButton\Parameters\GetRecordingsParameters;
|
use BigBlueButton\Parameters\GetRecordingsParameters;
|
||||||
|
use BigBlueButton\Parameters\InsertDocumentParameters;
|
||||||
use BigBlueButton\Parameters\IsMeetingRunningParameters;
|
use BigBlueButton\Parameters\IsMeetingRunningParameters;
|
||||||
use BigBlueButton\Parameters\JoinMeetingParameters;
|
use BigBlueButton\Parameters\JoinMeetingParameters;
|
||||||
use OCA\BigBlueButton\AppInfo\Application;
|
use OCA\BigBlueButton\AppInfo\Application;
|
||||||
|
@ -320,4 +321,14 @@ class API {
|
||||||
|
|
||||||
return $response->success() && $response->isRunning();
|
return $response->success() && $response->isRunning();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function insertDocument(Room $room, string $url, string $filename): bool {
|
||||||
|
$insertDocumentParams = new InsertDocumentParameters($room->getUid());
|
||||||
|
|
||||||
|
$insertDocumentParams->addPresentation($url, $filename, null, null);
|
||||||
|
|
||||||
|
$response = $this->getServer()->insertDocument($insertDocumentParams);
|
||||||
|
|
||||||
|
return $response->success();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,8 @@ class HookController extends Controller {
|
||||||
$recordingmarks = \boolval($recordingmarks);
|
$recordingmarks = \boolval($recordingmarks);
|
||||||
$room = $this->getRoom();
|
$room = $this->getRoom();
|
||||||
|
|
||||||
|
$this->service->updateRunning($room->getId(), false);
|
||||||
|
|
||||||
$this->avatarRepository->clearRoom($room->uid);
|
$this->avatarRepository->clearRoom($room->uid);
|
||||||
|
|
||||||
$this->eventDispatcher->dispatch(MeetingEndedEvent::class, new MeetingEndedEvent($room, $recordingmarks));
|
$this->eventDispatcher->dispatch(MeetingEndedEvent::class, new MeetingEndedEvent($room, $recordingmarks));
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace OCA\BigBlueButton\Controller;
|
namespace OCA\BigBlueButton\Controller;
|
||||||
|
|
||||||
|
use OCA\BigBlueButton\BackgroundJob\IsRunningJob;
|
||||||
use OCA\BigBlueButton\BigBlueButton\API;
|
use OCA\BigBlueButton\BigBlueButton\API;
|
||||||
use OCA\BigBlueButton\BigBlueButton\Presentation;
|
use OCA\BigBlueButton\BigBlueButton\Presentation;
|
||||||
use OCA\BigBlueButton\Db\Room;
|
use OCA\BigBlueButton\Db\Room;
|
||||||
|
@ -12,6 +13,7 @@ use OCA\BigBlueButton\Service\RoomService;
|
||||||
use OCP\AppFramework\Controller;
|
use OCP\AppFramework\Controller;
|
||||||
use OCP\AppFramework\Http\RedirectResponse;
|
use OCP\AppFramework\Http\RedirectResponse;
|
||||||
use OCP\AppFramework\Http\TemplateResponse;
|
use OCP\AppFramework\Http\TemplateResponse;
|
||||||
|
use OCP\BackgroundJob\IJobList;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
use OCP\IURLGenerator;
|
use OCP\IURLGenerator;
|
||||||
use OCP\IUserSession;
|
use OCP\IUserSession;
|
||||||
|
@ -38,6 +40,9 @@ class JoinController extends Controller {
|
||||||
/** @var Permission */
|
/** @var Permission */
|
||||||
private $permission;
|
private $permission;
|
||||||
|
|
||||||
|
/** @var IJobList */
|
||||||
|
private $jobList;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string $appName,
|
string $appName,
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
|
@ -45,7 +50,8 @@ class JoinController extends Controller {
|
||||||
IURLGenerator $urlGenerator,
|
IURLGenerator $urlGenerator,
|
||||||
IUserSession $userSession,
|
IUserSession $userSession,
|
||||||
API $api,
|
API $api,
|
||||||
Permission $permission
|
Permission $permission,
|
||||||
|
IJobList $jobList
|
||||||
) {
|
) {
|
||||||
parent::__construct($appName, $request);
|
parent::__construct($appName, $request);
|
||||||
|
|
||||||
|
@ -54,6 +60,7 @@ class JoinController extends Controller {
|
||||||
$this->userSession = $userSession;
|
$this->userSession = $userSession;
|
||||||
$this->api = $api;
|
$this->api = $api;
|
||||||
$this->permission = $permission;
|
$this->permission = $permission;
|
||||||
|
$this->jobList = $jobList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setToken(string $token): void {
|
public function setToken(string $token): void {
|
||||||
|
@ -129,6 +136,8 @@ class JoinController extends Controller {
|
||||||
$creationDate = $this->api->createMeeting($room, $presentation);
|
$creationDate = $this->api->createMeeting($room, $presentation);
|
||||||
$joinUrl = $this->api->createJoinUrl($room, $creationDate, $displayname, $isModerator, $userId);
|
$joinUrl = $this->api->createJoinUrl($room, $creationDate, $displayname, $isModerator, $userId);
|
||||||
|
|
||||||
|
$this->markAsRunning($room);
|
||||||
|
|
||||||
\OCP\Util::addHeader('meta', ['http-equiv' => 'refresh', 'content' => '3;url='.$joinUrl]);
|
\OCP\Util::addHeader('meta', ['http-equiv' => 'refresh', 'content' => '3;url='.$joinUrl]);
|
||||||
|
|
||||||
return new TemplateResponse($this->appName, 'forward', [
|
return new TemplateResponse($this->appName, 'forward', [
|
||||||
|
@ -153,4 +162,18 @@ class JoinController extends Controller {
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function markAsRunning(Room $room) {
|
||||||
|
if (!$room->running) {
|
||||||
|
$this->service->updateRunning($room->getId(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->jobList->has(IsRunningJob::class, [
|
||||||
|
'id' => $room->id,
|
||||||
|
])) {
|
||||||
|
$this->jobList->add(IsRunningJob::class, [
|
||||||
|
'id' => $room->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,44 @@ class ServerController extends Controller {
|
||||||
$this->userId = $UserId;
|
$this->userId = $UserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*/
|
||||||
|
public function isRunning(string $roomUid): DataResponse {
|
||||||
|
$room = $this->service->findByUid($roomUid);
|
||||||
|
|
||||||
|
if ($room === null) {
|
||||||
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->permission->isUser($room, $this->userId)) {
|
||||||
|
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
$isRunning = $this->server->isRunning($room);
|
||||||
|
|
||||||
|
return new DataResponse($isRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*/
|
||||||
|
public function insertDocument(string $roomUid, string $url, string $filename): DataResponse {
|
||||||
|
$room = $this->service->findByUid($roomUid);
|
||||||
|
|
||||||
|
if ($room === null) {
|
||||||
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->permission->isModerator($room, $this->userId)) {
|
||||||
|
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = $this->server->insertDocument($room, $url, $filename);
|
||||||
|
|
||||||
|
return new DataResponse($success);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -25,6 +25,7 @@ use OCP\AppFramework\Db\Entity;
|
||||||
* @method bool getMediaCheck()
|
* @method bool getMediaCheck()
|
||||||
* @method bool getCleanLayout()
|
* @method bool getCleanLayout()
|
||||||
* @method bool getJoinMuted()
|
* @method bool getJoinMuted()
|
||||||
|
* @method bool getRunning()
|
||||||
* @method void setUid(string $uid)
|
* @method void setUid(string $uid)
|
||||||
* @method void setName(string $name)
|
* @method void setName(string $name)
|
||||||
* @method void setAttendeePassword(string $pw)
|
* @method void setAttendeePassword(string $pw)
|
||||||
|
@ -42,6 +43,7 @@ use OCP\AppFramework\Db\Entity;
|
||||||
* @method void setMediaCheck(bool $mediaCheck)
|
* @method void setMediaCheck(bool $mediaCheck)
|
||||||
* @method void setCleanLayout(bool $cleanLayout)
|
* @method void setCleanLayout(bool $cleanLayout)
|
||||||
* @method void setJoinMuted(bool $joinMuted)
|
* @method void setJoinMuted(bool $joinMuted)
|
||||||
|
* @method void setRunning(bool $running)
|
||||||
*/
|
*/
|
||||||
class Room extends Entity implements JsonSerializable {
|
class Room extends Entity implements JsonSerializable {
|
||||||
public const ACCESS_PUBLIC = 'public';
|
public const ACCESS_PUBLIC = 'public';
|
||||||
|
@ -71,6 +73,7 @@ class Room extends Entity implements JsonSerializable {
|
||||||
public $mediaCheck;
|
public $mediaCheck;
|
||||||
public $cleanLayout;
|
public $cleanLayout;
|
||||||
public $joinMuted;
|
public $joinMuted;
|
||||||
|
public $running;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->addType('maxParticipants', 'integer');
|
$this->addType('maxParticipants', 'integer');
|
||||||
|
@ -82,6 +85,7 @@ class Room extends Entity implements JsonSerializable {
|
||||||
$this->addType('mediaCheck', 'boolean');
|
$this->addType('mediaCheck', 'boolean');
|
||||||
$this->addType('cleanLayout', 'boolean');
|
$this->addType('cleanLayout', 'boolean');
|
||||||
$this->addType('joinMuted', 'boolean');
|
$this->addType('joinMuted', 'boolean');
|
||||||
|
$this->addType('running', 'boolean');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function jsonSerialize(): array {
|
public function jsonSerialize(): array {
|
||||||
|
@ -103,6 +107,7 @@ class Room extends Entity implements JsonSerializable {
|
||||||
'mediaCheck' => boolval($this->mediaCheck),
|
'mediaCheck' => boolval($this->mediaCheck),
|
||||||
'cleanLayout' => boolval($this->cleanLayout),
|
'cleanLayout' => boolval($this->cleanLayout),
|
||||||
'joinMuted' => boolval($this->joinMuted),
|
'joinMuted' => boolval($this->joinMuted),
|
||||||
|
'running' => boolval($this->running),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace OCA\BigBlueButton\Migration;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use OCP\DB\ISchemaWrapper;
|
||||||
|
use OCP\Migration\IOutput;
|
||||||
|
use OCP\Migration\SimpleMigrationStep;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated migration step: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
class Version000000Date20220316151400 extends SimpleMigrationStep {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IOutput $output
|
||||||
|
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||||
|
* @param array $options
|
||||||
|
* @return null|ISchemaWrapper
|
||||||
|
*/
|
||||||
|
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||||
|
$schema = $schemaClosure();
|
||||||
|
|
||||||
|
if ($schema->hasTable('bbb_rooms')) {
|
||||||
|
$table = $schema->getTable('bbb_rooms');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('running')) {
|
||||||
|
$table->addColumn('running', 'boolean', [
|
||||||
|
'notnull' => false,
|
||||||
|
'default' => false
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -163,6 +163,21 @@ class RoomService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \OCP\AppFramework\Db\Entity|null
|
||||||
|
*/
|
||||||
|
public function updateRunning(int $id, bool $running) {
|
||||||
|
try {
|
||||||
|
$room = $this->mapper->find($id);
|
||||||
|
|
||||||
|
$room->setRunning($running);
|
||||||
|
|
||||||
|
return $this->mapper->update($room);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->handleException($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Room|null
|
* @return Room|null
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
"@commitlint/config-conventional": "^16.2.1",
|
"@commitlint/config-conventional": "^16.2.1",
|
||||||
"@commitlint/travis-cli": "^16.2.3",
|
"@commitlint/travis-cli": "^16.2.3",
|
||||||
"@nextcloud/axios": "^1.3.2",
|
"@nextcloud/axios": "^1.3.2",
|
||||||
|
"@nextcloud/dialogs": "^3.1.2",
|
||||||
"@nextcloud/router": "^2.0.0",
|
"@nextcloud/router": "^2.0.0",
|
||||||
"@octokit/rest": "^18.0.4",
|
"@octokit/rest": "^18.0.4",
|
||||||
"archiver": "^5.0.0",
|
"archiver": "^5.0.0",
|
||||||
|
|
|
@ -11,6 +11,7 @@ use OCA\BigBlueButton\Service\RoomService;
|
||||||
use OCP\AppFramework\Http;
|
use OCP\AppFramework\Http;
|
||||||
use OCP\AppFramework\Http\RedirectResponse;
|
use OCP\AppFramework\Http\RedirectResponse;
|
||||||
use OCP\AppFramework\Http\TemplateResponse;
|
use OCP\AppFramework\Http\TemplateResponse;
|
||||||
|
use OCP\BackgroundJob\IJobList;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
use OCP\IURLGenerator;
|
use OCP\IURLGenerator;
|
||||||
use OCP\IUser;
|
use OCP\IUser;
|
||||||
|
@ -26,6 +27,7 @@ class JoinControllerTest extends TestCase {
|
||||||
private $api;
|
private $api;
|
||||||
private $permission;
|
private $permission;
|
||||||
private $room;
|
private $room;
|
||||||
|
private $jobList;
|
||||||
|
|
||||||
public function setUp(): void {
|
public function setUp(): void {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
@ -36,6 +38,7 @@ class JoinControllerTest extends TestCase {
|
||||||
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
||||||
$this->api = $this->createMock(API::class);
|
$this->api = $this->createMock(API::class);
|
||||||
$this->permission = $this->createMock(Permission::class);
|
$this->permission = $this->createMock(Permission::class);
|
||||||
|
$this->jobList = $this->createMock(IJobList::class);
|
||||||
|
|
||||||
$this->controller = new JoinController(
|
$this->controller = new JoinController(
|
||||||
'bbb',
|
'bbb',
|
||||||
|
@ -44,10 +47,12 @@ class JoinControllerTest extends TestCase {
|
||||||
$this->urlGenerator,
|
$this->urlGenerator,
|
||||||
$this->userSession,
|
$this->userSession,
|
||||||
$this->api,
|
$this->api,
|
||||||
$this->permission
|
$this->permission,
|
||||||
|
$this->jobList
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->room = new Room();
|
$this->room = new Room();
|
||||||
|
$this->room->id = 1;
|
||||||
$this->room->uid = 'uid_foo';
|
$this->room->uid = 'uid_foo';
|
||||||
$this->room->userId = 'user_foo';
|
$this->room->userId = 'user_foo';
|
||||||
$this->room->access = Room::ACCESS_PUBLIC;
|
$this->room->access = Room::ACCESS_PUBLIC;
|
||||||
|
|
|
@ -44,6 +44,7 @@ export interface Room {
|
||||||
mediaCheck: boolean,
|
mediaCheck: boolean,
|
||||||
cleanLayout: boolean,
|
cleanLayout: boolean,
|
||||||
joinMuted: boolean,
|
joinMuted: boolean,
|
||||||
|
running: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomShare {
|
export interface RoomShare {
|
||||||
|
@ -132,6 +133,18 @@ class Api {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async isRunning(uid: string): Promise<boolean> {
|
||||||
|
const response = await axios.get(this.getUrl(`server/${uid}/isRunning`));
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insertDocument(uid: string, url: string, filename: string): Promise<boolean> {
|
||||||
|
const response = await axios.post(this.getUrl(`server/${uid}/insertDocument`), { url, filename });
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
public getRoomUrl(room: Room, forModerator = false) {
|
public getRoomUrl(room: Room, forModerator = false) {
|
||||||
const shortener = document.getElementById('bbb-root')?.getAttribute('data-shortener') || '';
|
const shortener = document.getElementById('bbb-root')?.getAttribute('data-shortener') || '';
|
||||||
const token = (forModerator && room.moderatorToken) ? `${room.uid}/${room.moderatorToken}` : room.uid;
|
const token = (forModerator && room.moderatorToken) ? `${room.uid}/${room.moderatorToken}` : room.uid;
|
||||||
|
@ -257,9 +270,9 @@ class Api {
|
||||||
groups: [],
|
groups: [],
|
||||||
circles: [],
|
circles: [],
|
||||||
exact: {
|
exact: {
|
||||||
users: response.data.ocs.data.exact.users,
|
users: response.data.ocs.data.exact.users,
|
||||||
groups: response.data.ocs.data.exact.groups,
|
groups: response.data.ocs.data.exact.groups,
|
||||||
circles: response.data.ocs.data.exact.circles || [],
|
circles: response.data.ocs.data.exact.circles || [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,6 +102,16 @@ pre {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button.success {
|
||||||
|
background-color: var(--color-success);
|
||||||
|
border-color: var(--color-success-hover);
|
||||||
|
color: var(--color-primary-text);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-success-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
@ -184,6 +194,10 @@ pre {
|
||||||
width: 42px;
|
width: 42px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.name {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tfoot td {
|
tfoot td {
|
||||||
|
|
|
@ -43,7 +43,7 @@ const App: React.FC<Props> = () => {
|
||||||
const [orderBy, setOrderBy] = useState<SortKey>('name');
|
const [orderBy, setOrderBy] = useState<SortKey>('name');
|
||||||
const [sortOrder, setSortOrder] = useState(SortOrder.ASC);
|
const [sortOrder, setSortOrder] = useState(SortOrder.ASC);
|
||||||
|
|
||||||
const rows = rooms.sort(sortRooms(orderBy, sortOrder)).map(room => <RoomRow room={room} restriction={restriction} key={room.id} updateRoom={updateRoom} deleteRoom={deleteRoom} />);
|
const rows = rooms.sort(sortRooms(orderBy, sortOrder)).map(room => <RoomRow room={room} restriction={restriction} key={room.id} updateRoom={updateRoom} deleteRoom={deleteRoom} cloneRoom={cloneRoom}/>);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
|
@ -105,13 +105,18 @@ const App: React.FC<Props> = () => {
|
||||||
|
|
||||||
function updateRoom(room: Room) {
|
function updateRoom(room: Room) {
|
||||||
return api.updateRoom(room).then(updatedRoom => {
|
return api.updateRoom(room).then(updatedRoom => {
|
||||||
setRooms(rooms.map(room => {
|
|
||||||
if (room.id === updatedRoom.id) {
|
|
||||||
return updatedRoom;
|
|
||||||
}
|
|
||||||
|
|
||||||
return room;
|
if (!rooms.find(room => room.id === updatedRoom.id)) {
|
||||||
}));
|
setRooms(rooms.concat([updatedRoom]));
|
||||||
|
} else {
|
||||||
|
setRooms(rooms.map(room => {
|
||||||
|
if (room.id === updatedRoom.id) {
|
||||||
|
return updatedRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
return room;
|
||||||
|
}));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,6 +126,25 @@ const App: React.FC<Props> = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cloneRoom(room: Room) {
|
||||||
|
|
||||||
|
if (room.moderatorToken !== null) {
|
||||||
|
room.moderatorToken = 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.createRoom(room.name, room.access, room.maxParticipants).then(newRoom => {
|
||||||
|
api.getRoomShares(room.id).then(shares => shares.forEach(share => {
|
||||||
|
api.createRoomShare(newRoom.id, share.shareType, share.shareWith, share.permission);
|
||||||
|
}));
|
||||||
|
|
||||||
|
updateRoom({
|
||||||
|
...room,
|
||||||
|
uid: newRoom.uid,
|
||||||
|
id: newRoom.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const maxRooms = restriction?.maxRooms || 0;
|
const maxRooms = restriction?.maxRooms || 0;
|
||||||
const quota = maxRooms < 0 ? t('bbb', 'unlimited') : rooms.filter(room => room.userId === OC.currentUser).length + ' / ' + maxRooms;
|
const quota = maxRooms < 0 ? t('bbb', 'unlimited') : rooms.filter(room => room.userId === OC.currentUser).length + ' / ' + maxRooms;
|
||||||
|
|
||||||
|
@ -151,6 +175,7 @@ const App: React.FC<Props> = () => {
|
||||||
</th>
|
</th>
|
||||||
<th />
|
<th />
|
||||||
<th />
|
<th />
|
||||||
|
<th />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
|
@ -11,6 +11,7 @@ type Props = {
|
||||||
restriction?: Restriction;
|
restriction?: Restriction;
|
||||||
updateRoom: (room: Room) => Promise<void>;
|
updateRoom: (room: Room) => Promise<void>;
|
||||||
deleteRoom: (id: number) => void;
|
deleteRoom: (id: number) => void;
|
||||||
|
cloneRoom: (room: Room) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingsNumberProps = {
|
type RecordingsNumberProps = {
|
||||||
|
@ -175,6 +176,10 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
return <EditableValue field={field} value={room[field]} setValue={updateRoom} type={type} options={options} />;
|
return <EditableValue field={field} value={room[field]} setValue={updateRoom} type={type} options={options} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cloneRow() {
|
||||||
|
props.cloneRoom({...props.room});
|
||||||
|
}
|
||||||
|
|
||||||
const avatarUrl = OC.generateUrl('/avatar/' + encodeURIComponent(room.userId) + '/' + 24, {
|
const avatarUrl = OC.generateUrl('/avatar/' + encodeURIComponent(room.userId) + '/' + 24, {
|
||||||
user: room.userId,
|
user: room.userId,
|
||||||
size: 24,
|
size: 24,
|
||||||
|
@ -187,9 +192,9 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr className={showRecordings ? 'selected-row' : ''}>
|
<tr className={showRecordings ? 'selected-row' : ''}>
|
||||||
<td className="start icon-col">
|
<td className="start">
|
||||||
<a href={api.getRoomUrl(room)} className="action-item" target="_blank" rel="noopener noreferrer" title={t('bbb', 'Open room')}>
|
<a href={api.getRoomUrl(room)} className={`button ${room.running ? 'success' : 'primary'}`} target="_blank" rel="noopener noreferrer" title={t('bbb', 'Open room')}>
|
||||||
<span className="icon icon-play icon-visible"></span>
|
{room.running ? t('bbb', 'Join') : t('bbb', 'Start')}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td className="share icon-col">
|
<td className="share icon-col">
|
||||||
|
@ -225,6 +230,11 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
<td className="edit icon-col">
|
<td className="edit icon-col">
|
||||||
<EditRoom room={props.room} restriction={props.restriction} updateProperty={updateRoom} />
|
<EditRoom room={props.room} restriction={props.restriction} updateProperty={updateRoom} />
|
||||||
</td>
|
</td>
|
||||||
|
<td className="clone icon-col">
|
||||||
|
<button className="action-item" onClick={cloneRow} title={t('bbb', 'Clone Room')}>
|
||||||
|
<span className="icon icon-template-add icon-visible"></span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
<td className="remove icon-col">
|
<td className="remove icon-col">
|
||||||
<button className="action-item" onClick={deleteRow as any} title={t('bbb', 'Delete')}>
|
<button className="action-item" onClick={deleteRow as any} title={t('bbb', 'Delete')}>
|
||||||
<span className="icon icon-delete icon-visible"></span>
|
<span className="icon icon-delete icon-visible"></span>
|
||||||
|
|
|
@ -73,6 +73,8 @@ declare namespace OC {
|
||||||
const PERMISSION_SHARE = 16;
|
const PERMISSION_SHARE = 16;
|
||||||
const PERMISSION_ALL = 31;
|
const PERMISSION_ALL = 31;
|
||||||
|
|
||||||
|
const debug: boolean;
|
||||||
|
|
||||||
const currentUser: string;
|
const currentUser: string;
|
||||||
|
|
||||||
function getCurrentUser(): {uid: string; displayName: string}
|
function getCurrentUser(): {uid: string; displayName: string}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
#bbb-file-action {
|
||||||
|
button.success {
|
||||||
|
background-color: var(--color-success);
|
||||||
|
border-color: var(--color-success-hover);
|
||||||
|
color: var(--color-primary-text);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-success-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
import axios from '@nextcloud/axios';
|
import axios from '@nextcloud/axios';
|
||||||
import { generateOcsUrl, generateUrl } from '@nextcloud/router';
|
import { generateOcsUrl, generateUrl } from '@nextcloud/router';
|
||||||
|
import { showSuccess, showWarning, showError } from '@nextcloud/dialogs';
|
||||||
|
import '@nextcloud/dialogs/styles/toast';
|
||||||
import { api } from './Common/Api';
|
import { api } from './Common/Api';
|
||||||
|
import './filelist.scss';
|
||||||
|
|
||||||
type OC_Dialogs_Message = (content: string, title: string, dialogType: 'notice' | 'alert' | 'warn' | 'none', buttons?: number, callback?: () => void, modal?: boolean, allowHtml?: boolean) => Promise<void>;
|
type OC_Dialogs_Message = (content: string, title: string, dialogType: 'notice' | 'alert' | 'warn' | 'none', buttons?: number, callback?: () => void, modal?: boolean, allowHtml?: boolean) => Promise<void>;
|
||||||
type ExtendedDialogs = typeof OC.dialogs & { message: OC_Dialogs_Message };
|
type ExtendedDialogs = typeof OC.dialogs & { message: OC_Dialogs_Message };
|
||||||
|
@ -38,8 +41,7 @@ async function createDirectShare(fileId: number): Promise<string> {
|
||||||
return createResponse.data?.ocs?.data?.url;
|
return createResponse.data?.ocs?.data?.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function share(fileId: number, filename: string, roomUid: string) {
|
async function createRoomWithFile(shareUrl: string, filename: string, roomUid: string) {
|
||||||
const shareUrl = await createDirectShare(fileId);
|
|
||||||
const joinUrl = generateUrl('/apps/bbb/b/{uid}?u={url}&filename={filename}', {
|
const joinUrl = generateUrl('/apps/bbb/b/{uid}?u={url}&filename={filename}', {
|
||||||
uid: roomUid,
|
uid: roomUid,
|
||||||
url: shareUrl,
|
url: shareUrl,
|
||||||
|
@ -49,6 +51,31 @@ async function share(fileId: number, filename: string, roomUid: string) {
|
||||||
window.open(joinUrl, '_blank', 'noopener,noreferrer');
|
window.open(joinUrl, '_blank', 'noopener,noreferrer');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function insertDocumentToRoom(shareUrl: string, filename: string, roomUid: string) {
|
||||||
|
return api.insertDocument(roomUid, shareUrl, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendFile(fileId: number, filename: string, roomUid: string) {
|
||||||
|
const shareUrl = await createDirectShare(fileId);
|
||||||
|
const isRunning = await api.isRunning(roomUid);
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
try {
|
||||||
|
const success = await insertDocumentToRoom(shareUrl, filename, roomUid);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
showSuccess(t('bbb', 'The file "{filename}" was uploaded to your room.', { filename }));
|
||||||
|
} else {
|
||||||
|
showWarning(t('bbb', 'The file "{filename}" could not be uploaded to your room.', { filename }));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showError(t('bbb', 'The file "{filename}" could not be uploaded to your room. Maybe your BigBlueButton server does not support this action.', { filename }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
createRoomWithFile(shareUrl, filename, roomUid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function openDialog(fileId: number, filename: string) {
|
async function openDialog(fileId: number, filename: string) {
|
||||||
const initContent = '<div id="bbb-file-action"><span className="icon icon-loading-small icon-visible"></span></div>';
|
const initContent = '<div id="bbb-file-action"><span className="icon icon-loading-small icon-visible"></span></div>';
|
||||||
const title = t('bbb', 'Send file to BBB');
|
const title = t('bbb', 'Send file to BBB');
|
||||||
|
@ -65,15 +92,21 @@ async function openDialog(fileId: number, filename: string) {
|
||||||
const row = $('<tr>');
|
const row = $('<tr>');
|
||||||
const button = $('<button>');
|
const button = $('<button>');
|
||||||
|
|
||||||
button.text(t('bbb', 'Start'));
|
if (!OC.debug) {
|
||||||
button.addClass('primary');
|
button.prop('disabled', room.running);
|
||||||
|
}
|
||||||
|
button.text(room.running ? t('bbb', 'Send to') : t('bbb', 'Start with'));
|
||||||
|
button.addClass(room.running ? 'success' : 'primary');
|
||||||
button.attr('type', 'button');
|
button.attr('type', 'button');
|
||||||
button.on('click', (ev) => {
|
button.on('click', (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
share(fileId, filename, room.uid);
|
table.find('button').prop('disabled', true);
|
||||||
|
$(ev.target).addClass('icon-loading-small');
|
||||||
|
|
||||||
container.parents('.oc-dialog').find('.oc-dialog-close').trigger('click');
|
sendFile(fileId, filename, room.uid).then(() => {
|
||||||
|
container.parents('.oc-dialog').find('.oc-dialog-close').trigger('click');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
row.append($('<td>').append(button));
|
row.append($('<td>').append(button));
|
||||||
|
|
27
yarn.lock
27
yarn.lock
|
@ -1375,6 +1375,16 @@
|
||||||
resolved "https://registry.yarnpkg.com/@nextcloud/browserslist-config/-/browserslist-config-2.2.0.tgz#85c2b9363b4aa98ea534576b9f9094cc0afa8f78"
|
resolved "https://registry.yarnpkg.com/@nextcloud/browserslist-config/-/browserslist-config-2.2.0.tgz#85c2b9363b4aa98ea534576b9f9094cc0afa8f78"
|
||||||
integrity sha512-kC42RQW5rZjZZsRaEjVlIQpp6aW/yxm+zZdETnrRQnUzcPwBgF4wO4makfGT63Ckd+LkgUW+geesPiPRqxFVew==
|
integrity sha512-kC42RQW5rZjZZsRaEjVlIQpp6aW/yxm+zZdETnrRQnUzcPwBgF4wO4makfGT63Ckd+LkgUW+geesPiPRqxFVew==
|
||||||
|
|
||||||
|
"@nextcloud/dialogs@^3.1.2":
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@nextcloud/dialogs/-/dialogs-3.1.2.tgz#84c6b9dd13f757863a2d95215e00a161d6bb654a"
|
||||||
|
integrity sha512-hVgpr/CF0F+cE7tRZHJDVpB1S05K/pDcUMrfDpoxMKhux5SXlpwLXUaWM7iAbHEKYm6ArWdpUyhxBTTAo9yrvg==
|
||||||
|
dependencies:
|
||||||
|
"@nextcloud/l10n" "^1.3.0"
|
||||||
|
"@nextcloud/typings" "^1.0.0"
|
||||||
|
core-js "^3.6.4"
|
||||||
|
toastify-js "^1.10.0"
|
||||||
|
|
||||||
"@nextcloud/eslint-plugin@^2.0.0":
|
"@nextcloud/eslint-plugin@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@nextcloud/eslint-plugin/-/eslint-plugin-2.0.0.tgz#55336973752af669954b0cf0c8887503e8494ba6"
|
resolved "https://registry.yarnpkg.com/@nextcloud/eslint-plugin/-/eslint-plugin-2.0.0.tgz#55336973752af669954b0cf0c8887503e8494ba6"
|
||||||
|
@ -1421,6 +1431,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/jquery" "2.0.54"
|
"@types/jquery" "2.0.54"
|
||||||
|
|
||||||
|
"@nextcloud/typings@^1.0.0":
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@nextcloud/typings/-/typings-1.3.0.tgz#8898d3a31c10a31b409c6815560285f047f699cc"
|
||||||
|
integrity sha512-lxAWXobayeaAnydCWKMljJXDLXECimUf3HbTjnrjL7pslPyGyH70LHj4NZ5BmGbKeU2yjF2FTXIGji10lOryiQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/jquery" "2.0.57"
|
||||||
|
|
||||||
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3":
|
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3":
|
||||||
version "2.1.8-no-fsevents.3"
|
version "2.1.8-no-fsevents.3"
|
||||||
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b"
|
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b"
|
||||||
|
@ -1651,6 +1668,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.54.tgz#d7999245f77c3fab5d84e7d32b8a6c20bfd1f072"
|
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.54.tgz#d7999245f77c3fab5d84e7d32b8a6c20bfd1f072"
|
||||||
integrity sha512-D/PomKwNkDfSKD13DEVQT/pq2TUjN54c6uB341fEZanIzkjfGe7UaFuuaLZbpEiS5j7Wk2MUHAZqZIoECw29lg==
|
integrity sha512-D/PomKwNkDfSKD13DEVQT/pq2TUjN54c6uB341fEZanIzkjfGe7UaFuuaLZbpEiS5j7Wk2MUHAZqZIoECw29lg==
|
||||||
|
|
||||||
|
"@types/jquery@2.0.57":
|
||||||
|
version "2.0.57"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.57.tgz#df4241708df642021c5c715de64207371240c6e5"
|
||||||
|
integrity sha512-QUJ5wVL8iTZofgZjCfVnHxcMqqPPLfVfEKe8rfksMdmSmqEenpcpEBQO45VSSfng/tunwoLF+3I8rzEzVhYNLQ==
|
||||||
|
|
||||||
"@types/jquery@^3.3.35":
|
"@types/jquery@^3.3.35":
|
||||||
version "3.5.14"
|
version "3.5.14"
|
||||||
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.14.tgz#ac8e11ee591e94d4d58da602cb3a5a8320dee577"
|
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.14.tgz#ac8e11ee591e94d4d58da602cb3a5a8320dee577"
|
||||||
|
@ -6569,6 +6591,11 @@ to-regex-range@^5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-number "^7.0.0"
|
is-number "^7.0.0"
|
||||||
|
|
||||||
|
toastify-js@^1.10.0:
|
||||||
|
version "1.11.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/toastify-js/-/toastify-js-1.11.2.tgz#5c66cb05cee30b09e5bd8961ab809e41a3ce716c"
|
||||||
|
integrity sha512-bMBNKhZLPX/sDhpwM7KHIRUTtqCylQeoZDiEWy5zE7iDUJ92XmP8AKgDAp9rXx6pR5GXGFtQHHoH62toahbHgQ==
|
||||||
|
|
||||||
toggle-selection@^1.0.6:
|
toggle-selection@^1.0.6:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
|
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
|
||||||
|
|
Loading…
Reference in New Issue