mirror of https://github.com/sualko/cloud_bbb
Merge pull request #281 from arawa/feature/share_moderators_and_users
Feature/share moderators and userspull/285/head
commit
29aa147ce6
|
@ -14,6 +14,7 @@ return [
|
||||||
['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'],
|
||||||
['name' => 'server#delete_record', 'url' => '/server/record/{recordId}', 'verb' => 'DELETE'],
|
['name' => 'server#delete_record', 'url' => '/server/record/{recordId}', 'verb' => 'DELETE'],
|
||||||
|
['name' => 'server#publish_record', 'url' => '/server/record/{recordId}/publish', 'verb' => 'POST'],
|
||||||
['name' => 'join#index', 'url' => '/b/{token}/{moderatorToken}', 'verb' => 'GET', 'defaults' => ['moderatorToken' => '']],
|
['name' => 'join#index', 'url' => '/b/{token}/{moderatorToken}', 'verb' => 'GET', 'defaults' => ['moderatorToken' => '']],
|
||||||
['name' => 'restriction#user', 'url' => '/restrictions/user', 'verb' => 'GET'],
|
['name' => 'restriction#user', 'url' => '/restrictions/user', 'verb' => 'GET'],
|
||||||
['name' => 'hook#meetingEnded', 'url' => '/hook/ended/{token}/{mac}', 'verb' => 'GET'],
|
['name' => 'hook#meetingEnded', 'url' => '/hook/ended/{token}/{mac}', 'verb' => 'GET'],
|
||||||
|
|
|
@ -10,6 +10,7 @@ use BigBlueButton\Parameters\GetRecordingsParameters;
|
||||||
use BigBlueButton\Parameters\InsertDocumentParameters;
|
use BigBlueButton\Parameters\InsertDocumentParameters;
|
||||||
use BigBlueButton\Parameters\IsMeetingRunningParameters;
|
use BigBlueButton\Parameters\IsMeetingRunningParameters;
|
||||||
use BigBlueButton\Parameters\JoinMeetingParameters;
|
use BigBlueButton\Parameters\JoinMeetingParameters;
|
||||||
|
use BigBlueButton\Parameters\PublishRecordingsParameters;
|
||||||
use OCA\BigBlueButton\AppInfo\Application;
|
use OCA\BigBlueButton\AppInfo\Application;
|
||||||
use OCA\BigBlueButton\AvatarRepository;
|
use OCA\BigBlueButton\AvatarRepository;
|
||||||
use OCA\BigBlueButton\Crypto;
|
use OCA\BigBlueButton\Crypto;
|
||||||
|
@ -262,6 +263,14 @@ class API {
|
||||||
return $response->isDeleted();
|
return $response->isDeleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function publishRecording(string $recordingId, bool $published): bool {
|
||||||
|
$publishParams = new PublishRecordingsParameters($recordingId, $published);
|
||||||
|
|
||||||
|
$response = $this->getServer()->publishRecordings($publishParams);
|
||||||
|
|
||||||
|
return $response->isPublished();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return (array|bool|int|string)[]
|
* @return (array|bool|int|string)[]
|
||||||
*
|
*
|
||||||
|
|
|
@ -11,6 +11,7 @@ use OCP\AppFramework\Controller;
|
||||||
use OCP\AppFramework\Http;
|
use OCP\AppFramework\Http;
|
||||||
use OCP\AppFramework\Http\DataResponse;
|
use OCP\AppFramework\Http\DataResponse;
|
||||||
|
|
||||||
|
use OCP\IGroupManager;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
use OCP\IUserManager;
|
use OCP\IUserManager;
|
||||||
|
|
||||||
|
@ -24,6 +25,9 @@ class RoomShareController extends Controller {
|
||||||
/** @var IUserManager */
|
/** @var IUserManager */
|
||||||
private $userManager;
|
private $userManager;
|
||||||
|
|
||||||
|
/** @var IGroupManager */
|
||||||
|
private $groupManager;
|
||||||
|
|
||||||
/** @var RoomService */
|
/** @var RoomService */
|
||||||
private $roomService;
|
private $roomService;
|
||||||
|
|
||||||
|
@ -37,6 +41,7 @@ class RoomShareController extends Controller {
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
RoomShareService $service,
|
RoomShareService $service,
|
||||||
IUserManager $userManager,
|
IUserManager $userManager,
|
||||||
|
IGroupManager $groupManager,
|
||||||
RoomService $roomService,
|
RoomService $roomService,
|
||||||
CircleHelper $circleHelper,
|
CircleHelper $circleHelper,
|
||||||
$userId
|
$userId
|
||||||
|
@ -44,6 +49,7 @@ class RoomShareController extends Controller {
|
||||||
parent::__construct($appName, $request);
|
parent::__construct($appName, $request);
|
||||||
$this->service = $service;
|
$this->service = $service;
|
||||||
$this->userManager = $userManager;
|
$this->userManager = $userManager;
|
||||||
|
$this->groupManager = $groupManager;
|
||||||
$this->roomService = $roomService;
|
$this->roomService = $roomService;
|
||||||
$this->circleHelper = $circleHelper;
|
$this->circleHelper = $circleHelper;
|
||||||
$this->userId = $userId;
|
$this->userId = $userId;
|
||||||
|
@ -90,6 +96,14 @@ class RoomShareController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
$roomShare->setShareWithDisplayName($circle->getName());
|
$roomShare->setShareWithDisplayName($circle->getName());
|
||||||
|
} elseif ($roomShare->getShareType() === RoomShare::SHARE_TYPE_GROUP) {
|
||||||
|
$shareWithGroup = $this->groupManager->get($roomShare->getShareWith());
|
||||||
|
|
||||||
|
if ($shareWithGroup === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roomShare->setShareWithDisplayName($shareWithGroup->getDisplayName());
|
||||||
}
|
}
|
||||||
|
|
||||||
$shares[] = $roomShare;
|
$shares[] = $roomShare;
|
||||||
|
|
|
@ -88,12 +88,18 @@ class ServerController extends Controller {
|
||||||
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
return new DataResponse([], Http::STATUS_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->permission->isAdmin($room, $this->userId)) {
|
if (!$this->permission->isUser($room, $this->userId)) {
|
||||||
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
return new DataResponse([], Http::STATUS_FORBIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
$recordings = $this->server->getRecordings($room);
|
$recordings = $this->server->getRecordings($room);
|
||||||
|
|
||||||
|
if (!$this->permission->isAdmin($room, $this->userId)) {
|
||||||
|
$recordings = array_values(array_filter($recordings, function ($recording) {
|
||||||
|
return $recording['published'];
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return new DataResponse($recordings);
|
return new DataResponse($recordings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,6 +124,27 @@ class ServerController extends Controller {
|
||||||
return new DataResponse($success);
|
return new DataResponse($success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*/
|
||||||
|
public function publishRecord(string $recordId, bool $published): DataResponse {
|
||||||
|
$record = $this->server->getRecording($recordId);
|
||||||
|
|
||||||
|
$room = $this->service->findByUid($record['meetingId']);
|
||||||
|
|
||||||
|
if ($room === null) {
|
||||||
|
return new DataResponse(false, Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->permission->isAdmin($room, $this->userId)) {
|
||||||
|
return new DataResponse(false, Http::STATUS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = $this->server->publishRecording($recordId, $published);
|
||||||
|
|
||||||
|
return new DataResponse($success);
|
||||||
|
}
|
||||||
|
|
||||||
public function check(?string $url, ?string $secret): DataResponse {
|
public function check(?string $url, ?string $secret): DataResponse {
|
||||||
if ($url === null || empty($url) || $secret === null || empty($secret)) {
|
if ($url === null || empty($url) || $secret === null || empty($secret)) {
|
||||||
return new DataResponse(false);
|
return new DataResponse(false);
|
||||||
|
|
|
@ -74,6 +74,7 @@ class Room extends Entity implements JsonSerializable {
|
||||||
public $cleanLayout;
|
public $cleanLayout;
|
||||||
public $joinMuted;
|
public $joinMuted;
|
||||||
public $running;
|
public $running;
|
||||||
|
public $permission;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->addType('maxParticipants', 'integer');
|
$this->addType('maxParticipants', 'integer');
|
||||||
|
@ -86,6 +87,7 @@ class Room extends Entity implements JsonSerializable {
|
||||||
$this->addType('cleanLayout', 'boolean');
|
$this->addType('cleanLayout', 'boolean');
|
||||||
$this->addType('joinMuted', 'boolean');
|
$this->addType('joinMuted', 'boolean');
|
||||||
$this->addType('running', 'boolean');
|
$this->addType('running', 'boolean');
|
||||||
|
$this->addType('permission', 'integer');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function jsonSerialize(): array {
|
public function jsonSerialize(): array {
|
||||||
|
@ -102,6 +104,7 @@ class Room extends Entity implements JsonSerializable {
|
||||||
'everyoneIsModerator' => boolval($this->everyoneIsModerator),
|
'everyoneIsModerator' => boolval($this->everyoneIsModerator),
|
||||||
'requireModerator' => boolval($this->requireModerator),
|
'requireModerator' => boolval($this->requireModerator),
|
||||||
'shared' => boolval($this->shared),
|
'shared' => boolval($this->shared),
|
||||||
|
'permission' => $this->permission,
|
||||||
'moderatorToken' => $this->moderatorToken,
|
'moderatorToken' => $this->moderatorToken,
|
||||||
'listenOnly' => boolval($this->listenOnly),
|
'listenOnly' => boolval($this->listenOnly),
|
||||||
'mediaCheck' => boolval($this->mediaCheck),
|
'mediaCheck' => boolval($this->mediaCheck),
|
||||||
|
|
|
@ -12,6 +12,16 @@ class RoomMapper extends QBMapper {
|
||||||
parent::__construct($db, 'bbb_rooms', Room::class);
|
parent::__construct($db, 'bbb_rooms', Room::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function joinShares(IQueryBuilder $qb): IQueryBuilder {
|
||||||
|
$qb->select('r.*')
|
||||||
|
->from($this->tableName, 'r')
|
||||||
|
->leftJoin('r', 'bbb_room_shares', 's', $qb->expr()->eq('r.id', 's.room_id'))
|
||||||
|
->addSelect($qb->createFunction('count(case when `s`.`permission` IN ('.
|
||||||
|
RoomShare::PERMISSION_ADMIN.','.RoomShare::PERMISSION_MODERATOR.','.RoomShare::PERMISSION_USER
|
||||||
|
.') then 1 else null end) as shared'));
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||||
* @throws DoesNotExistException
|
* @throws DoesNotExistException
|
||||||
|
@ -19,10 +29,7 @@ class RoomMapper extends QBMapper {
|
||||||
public function find(int $id): Room {
|
public function find(int $id): Room {
|
||||||
/* @var $qb IQueryBuilder */
|
/* @var $qb IQueryBuilder */
|
||||||
$qb = $this->db->getQueryBuilder();
|
$qb = $this->db->getQueryBuilder();
|
||||||
$qb->select('r.*')
|
$this->joinShares($qb)
|
||||||
->from($this->tableName, 'r')
|
|
||||||
->leftJoin('r', 'bbb_room_shares', 's', $qb->expr()->eq('r.id', 's.room_id'))
|
|
||||||
->addSelect($qb->createFunction('count(case when `s`.`permission` = 0 then 1 else null end) as shared'))
|
|
||||||
->where($qb->expr()->eq('r.id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
|
->where($qb->expr()->eq('r.id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
|
||||||
->groupBy('r.id');
|
->groupBy('r.id');
|
||||||
;
|
;
|
||||||
|
@ -38,10 +45,7 @@ class RoomMapper extends QBMapper {
|
||||||
public function findByUid(string $uid): Room {
|
public function findByUid(string $uid): Room {
|
||||||
/* @var $qb IQueryBuilder */
|
/* @var $qb IQueryBuilder */
|
||||||
$qb = $this->db->getQueryBuilder();
|
$qb = $this->db->getQueryBuilder();
|
||||||
$qb->select('r.*')
|
$this->joinShares($qb)
|
||||||
->from($this->tableName, 'r')
|
|
||||||
->leftJoin('r', 'bbb_room_shares', 's', $qb->expr()->eq('r.id', 's.room_id'))
|
|
||||||
->addSelect($qb->createFunction('count(case when `s`.`permission` = 0 then 1 else null end) as shared'))
|
|
||||||
->where($qb->expr()->eq('r.uid', $qb->createNamedParameter($uid)))
|
->where($qb->expr()->eq('r.uid', $qb->createNamedParameter($uid)))
|
||||||
->groupBy('r.id');
|
->groupBy('r.id');
|
||||||
;
|
;
|
||||||
|
@ -70,25 +74,20 @@ class RoomMapper extends QBMapper {
|
||||||
public function findAll(string $userId, array $groupIds, array $circleIds): array {
|
public function findAll(string $userId, array $groupIds, array $circleIds): array {
|
||||||
/* @var $qb IQueryBuilder */
|
/* @var $qb IQueryBuilder */
|
||||||
$qb = $this->db->getQueryBuilder();
|
$qb = $this->db->getQueryBuilder();
|
||||||
$qb->select('r.*')
|
$this->joinShares($qb)
|
||||||
->from($this->tableName, 'r')
|
->addSelect($qb->createFunction('min(case when '.$qb->expr()->eq('r.user_id', $qb->createNamedParameter($userId)).' then '.RoomShare::PERMISSION_ADMIN.' else `s`.`permission` end) as permission'))
|
||||||
->leftJoin('r', 'bbb_room_shares', 's', $qb->expr()->eq('r.id', 's.room_id'))
|
|
||||||
->addSelect($qb->createFunction('count(case when `s`.`permission` = 0 then 1 else null end) as shared'))
|
|
||||||
->where(
|
->where(
|
||||||
$qb->expr()->orX(
|
$qb->expr()->orX(
|
||||||
$qb->expr()->eq('r.user_id', $qb->createNamedParameter($userId)),
|
$qb->expr()->eq('r.user_id', $qb->createNamedParameter($userId)),
|
||||||
$qb->expr()->andX(
|
$qb->expr()->andX(
|
||||||
$qb->expr()->eq('s.permission', $qb->createNamedParameter(RoomShare::PERMISSION_ADMIN, IQueryBuilder::PARAM_INT)),
|
|
||||||
$qb->expr()->eq('s.share_type', $qb->createNamedParameter(RoomShare::SHARE_TYPE_USER, IQueryBuilder::PARAM_INT)),
|
$qb->expr()->eq('s.share_type', $qb->createNamedParameter(RoomShare::SHARE_TYPE_USER, IQueryBuilder::PARAM_INT)),
|
||||||
$qb->expr()->eq('s.share_with', $qb->createNamedParameter($userId))
|
$qb->expr()->eq('s.share_with', $qb->createNamedParameter($userId))
|
||||||
),
|
),
|
||||||
$qb->expr()->andX(
|
$qb->expr()->andX(
|
||||||
$qb->expr()->eq('s.permission', $qb->createNamedParameter(RoomShare::PERMISSION_ADMIN, IQueryBuilder::PARAM_INT)),
|
|
||||||
$qb->expr()->eq('s.share_type', $qb->createNamedParameter(RoomShare::SHARE_TYPE_GROUP, IQueryBuilder::PARAM_INT)),
|
$qb->expr()->eq('s.share_type', $qb->createNamedParameter(RoomShare::SHARE_TYPE_GROUP, IQueryBuilder::PARAM_INT)),
|
||||||
$qb->expr()->in('s.share_with', $qb->createNamedParameter($groupIds, IQueryBuilder::PARAM_STR_ARRAY))
|
$qb->expr()->in('s.share_with', $qb->createNamedParameter($groupIds, IQueryBuilder::PARAM_STR_ARRAY))
|
||||||
),
|
),
|
||||||
$qb->expr()->andX(
|
$qb->expr()->andX(
|
||||||
$qb->expr()->eq('s.permission', $qb->createNamedParameter(RoomShare::PERMISSION_ADMIN, IQueryBuilder::PARAM_INT)),
|
|
||||||
$qb->expr()->eq('s.share_type', $qb->createNamedParameter(RoomShare::SHARE_TYPE_CIRCLE, IQueryBuilder::PARAM_INT)),
|
$qb->expr()->eq('s.share_type', $qb->createNamedParameter(RoomShare::SHARE_TYPE_CIRCLE, IQueryBuilder::PARAM_INT)),
|
||||||
$qb->expr()->in('s.share_with', $qb->createNamedParameter($circleIds, IQueryBuilder::PARAM_STR_ARRAY))
|
$qb->expr()->in('s.share_with', $qb->createNamedParameter($circleIds, IQueryBuilder::PARAM_STR_ARRAY))
|
||||||
)
|
)
|
||||||
|
@ -99,7 +98,7 @@ class RoomMapper extends QBMapper {
|
||||||
/** @var array<Room> */
|
/** @var array<Room> */
|
||||||
return $this->findEntities($qb);
|
return $this->findEntities($qb);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<Room>
|
* @return array<Room>
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -9,6 +9,7 @@ use OCA\BigBlueButton\Db\RoomShare;
|
||||||
use OCA\BigBlueButton\Service\RoomService;
|
use OCA\BigBlueButton\Service\RoomService;
|
||||||
use OCA\BigBlueButton\Service\RoomShareService;
|
use OCA\BigBlueButton\Service\RoomShareService;
|
||||||
use OCP\AppFramework\Http;
|
use OCP\AppFramework\Http;
|
||||||
|
use OCP\IGroupManager;
|
||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
use OCP\IUserManager;
|
use OCP\IUserManager;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
@ -19,6 +20,7 @@ class RoomShareControllerTest extends TestCase {
|
||||||
private $roomService;
|
private $roomService;
|
||||||
private $circleHelper;
|
private $circleHelper;
|
||||||
private $userManager;
|
private $userManager;
|
||||||
|
private $groupManager;
|
||||||
private $controller;
|
private $controller;
|
||||||
|
|
||||||
private $userId = 'user_foo';
|
private $userId = 'user_foo';
|
||||||
|
@ -29,6 +31,7 @@ class RoomShareControllerTest extends TestCase {
|
||||||
$this->request = $this->createMock(IRequest::class);
|
$this->request = $this->createMock(IRequest::class);
|
||||||
$this->service = $this->createMock(RoomShareService::class);
|
$this->service = $this->createMock(RoomShareService::class);
|
||||||
$this->userManager = $this->createMock(IUserManager::class);
|
$this->userManager = $this->createMock(IUserManager::class);
|
||||||
|
$this->groupManager = $this->createMock(IGroupManager::class);
|
||||||
$this->roomService = $this->createMock(RoomService::class);
|
$this->roomService = $this->createMock(RoomService::class);
|
||||||
$this->circleHelper = $this->createMock(CircleHelper::class);
|
$this->circleHelper = $this->createMock(CircleHelper::class);
|
||||||
|
|
||||||
|
@ -37,6 +40,7 @@ class RoomShareControllerTest extends TestCase {
|
||||||
$this->request,
|
$this->request,
|
||||||
$this->service,
|
$this->service,
|
||||||
$this->userManager,
|
$this->userManager,
|
||||||
|
$this->groupManager,
|
||||||
$this->roomService,
|
$this->roomService,
|
||||||
$this->circleHelper,
|
$this->circleHelper,
|
||||||
$this->userId
|
$this->userId
|
||||||
|
|
|
@ -39,6 +39,7 @@ export interface Room {
|
||||||
everyoneIsModerator: boolean;
|
everyoneIsModerator: boolean;
|
||||||
requireModerator: boolean;
|
requireModerator: boolean;
|
||||||
shared: boolean;
|
shared: boolean;
|
||||||
|
permission: Permission;
|
||||||
moderatorToken: string;
|
moderatorToken: string;
|
||||||
listenOnly: boolean,
|
listenOnly: boolean,
|
||||||
mediaCheck: boolean,
|
mediaCheck: boolean,
|
||||||
|
@ -200,6 +201,14 @@ class Api {
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async publishRecording(id: string, publish: boolean,) {
|
||||||
|
const response = await axios.post(this.getUrl(`server/record/${id}/publish`), {
|
||||||
|
published: publish,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
public async storeRecording(recording: Recording, path: string) {
|
public async storeRecording(recording: Recording, path: string) {
|
||||||
const startDate = new Date(recording.startTime);
|
const startDate = new Date(recording.startTime);
|
||||||
const filename = `${encodeURIComponent(recording.name + ' ' + startDate.toISOString())}.url`;
|
const filename = `${encodeURIComponent(recording.name + ' ' + startDate.toISOString())}.url`;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Access } from './Api';
|
import { Access, Permission } from './Api';
|
||||||
|
|
||||||
export const AccessOptions = {
|
export const AccessOptions = {
|
||||||
[Access.Public]: t('bbb', 'Public'),
|
[Access.Public]: t('bbb', 'Public'),
|
||||||
|
@ -8,3 +8,9 @@ export const AccessOptions = {
|
||||||
[Access.Internal]: t('bbb', 'Internal'),
|
[Access.Internal]: t('bbb', 'Internal'),
|
||||||
[Access.InternalRestricted]: t('bbb', 'Internal restricted'),
|
[Access.InternalRestricted]: t('bbb', 'Internal restricted'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PermissionsOptions = {
|
||||||
|
[Permission.Admin]: t('bbb', 'admin'),
|
||||||
|
[Permission.Moderator]: t('bbb', 'moderator'),
|
||||||
|
[Permission.User]: t('bbb', 'user'),
|
||||||
|
};
|
||||||
|
|
|
@ -161,8 +161,23 @@ pre {
|
||||||
.bbb-shrink {
|
.bbb-shrink {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
|
||||||
|
|
||||||
|
input[type="checkbox"]{
|
||||||
|
|
||||||
|
&+label:before {
|
||||||
|
border-radius: 3px;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled+label:before {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(input:checked):disabled+label:before {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
padding: 14px 6px;
|
padding: 14px 6px;
|
||||||
|
@ -282,6 +297,10 @@ pre {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bbb-simple-menu {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.bbb-input-container {
|
.bbb-input-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,17 +123,18 @@ const EditRoomDialog: React.FC<Props> = ({ room, restriction, updateProperty, op
|
||||||
updateProperty('access', value);
|
updateProperty('access', value);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{room.access === Access.InternalRestricted && <div className="bbb-form-element bbb-form-shareWith">
|
|
||||||
<ShareWith permission={Permission.User} room={room} shares={shares} setShares={setShares} />
|
|
||||||
<em>{descriptions.internalRestrictedShareWith}</em>
|
|
||||||
</div>}
|
|
||||||
|
|
||||||
<div className="bbb-form-element">
|
<div className="bbb-form-element">
|
||||||
<label htmlFor={'bbb-moderator'}>
|
<label htmlFor={'bbb-sharing'}>
|
||||||
<h3>Moderator</h3>
|
<h3>{t('bbb', 'Sharing')}</h3>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{!room.everyoneIsModerator && <ShareWith permission={Permission.Moderator} room={room} shares={shares} setShares={setShares} />}
|
{<ShareWith permission={Permission.User} room={room} shares={shares} setShares={setShares} />}
|
||||||
|
|
||||||
|
{room.access === Access.InternalRestricted &&
|
||||||
|
<div className="bbb-form-element bbb-form-shareWith">
|
||||||
|
<span className="icon icon-details icon-visible"></span><em>{t('bbb', 'Access') + ' : ' + descriptions.internalRestrictedShareWith}</em>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div className="bbb-mt-1">
|
<div className="bbb-mt-1">
|
||||||
<input id={'bbb-everyoneIsModerator-' + room.id}
|
<input id={'bbb-everyoneIsModerator-' + room.id}
|
||||||
|
|
|
@ -4,11 +4,29 @@ import { Recording } from '../Common/Api';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
recording: Recording;
|
recording: Recording;
|
||||||
|
isAdmin : boolean;
|
||||||
deleteRecording: (recording: Recording) => void;
|
deleteRecording: (recording: Recording) => void;
|
||||||
storeRecording: (recording: Recording) => void;
|
storeRecording: (recording: Recording) => void;
|
||||||
|
publishRecording: (recording: Recording, publish: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecordingRow: React.FC<Props> = ({recording, deleteRecording, storeRecording}) => {
|
const RecordingRow: React.FC<Props> = ({recording, isAdmin, deleteRecording, storeRecording, publishRecording}) => {
|
||||||
|
|
||||||
|
|
||||||
|
function checkPublished(recording: Recording, onChange: (value: boolean) => void) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input id={'bbb-record-state-' + recording.id}
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox"
|
||||||
|
checked={recording.state === 'published'}
|
||||||
|
onChange={(event) => onChange(event.target.checked)} />
|
||||||
|
<label htmlFor={'bbb-record-state-' + recording.id}>{t('bbb', 'Published')}</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={recording.id}>
|
<tr key={recording.id}>
|
||||||
<td className="start icon-col">
|
<td className="start icon-col">
|
||||||
|
@ -40,10 +58,17 @@ const RecordingRow: React.FC<Props> = ({recording, deleteRecording, storeRecordi
|
||||||
<td>
|
<td>
|
||||||
{recording.type}
|
{recording.type}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{isAdmin && checkPublished(recording, (checked) => {
|
||||||
|
publishRecording(recording, checked);
|
||||||
|
})}
|
||||||
|
</td>
|
||||||
<td className="remove icon-col">
|
<td className="remove icon-col">
|
||||||
<button className="action-item" onClick={() => deleteRecording(recording)} title={t('bbb', 'Delete')}>
|
{isAdmin &&
|
||||||
<span className="icon icon-delete icon-visible"></span>
|
<button className="action-item" onClick={() => deleteRecording(recording)} title={t('bbb', 'Delete')}>
|
||||||
</button>
|
<span className="icon icon-delete icon-visible"></span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
import { api, Recording, Room, Restriction, Access } from '../Common/Api';
|
import { api, Recording, Room, Restriction, Access, Permission } from '../Common/Api';
|
||||||
import EditRoom from './EditRoom';
|
import EditRoom from './EditRoom';
|
||||||
import RecordingRow from './RecordingRow';
|
import RecordingRow from './RecordingRow';
|
||||||
import EditableValue from './EditableValue';
|
import EditableValue from './EditableValue';
|
||||||
|
@ -155,6 +155,28 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function publishRecording(recording: Recording, publish: boolean) {
|
||||||
|
api.publishRecording(recording.id, publish).then(success=> {
|
||||||
|
if (recordings === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRecordings(recordings.map(recordItem => {
|
||||||
|
if (recordItem.id === recording.id) {
|
||||||
|
recordItem.published = success;
|
||||||
|
recordItem.state = success ? 'published' : 'unpublished';
|
||||||
|
}
|
||||||
|
return recordItem;
|
||||||
|
}));
|
||||||
|
}).catch(err => {
|
||||||
|
console.warn('Could not modify publishing state', err);
|
||||||
|
OC.dialogs.info(
|
||||||
|
t('bbb', 'Could not modify publishing state'),
|
||||||
|
t('bbb', 'Server error'),
|
||||||
|
() => undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function accessToIcon(access: string) {
|
function accessToIcon(access: string) {
|
||||||
switch(access) {
|
switch(access) {
|
||||||
case Access.Public:
|
case Access.Public:
|
||||||
|
@ -172,8 +194,11 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
return <span></span>;
|
return <span></span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(field: string, type: 'text' | 'number' = 'text', options?) {
|
function edit(field: string, type: 'text' | 'number' = 'text', canEdit = true, options?) {
|
||||||
return <EditableValue field={field} value={room[field]} setValue={updateRoom} type={type} options={options} />;
|
return canEdit ?
|
||||||
|
<EditableValue field={field} value={room[field]} setValue={updateRoom} type={type} options={options} />
|
||||||
|
:
|
||||||
|
<span>{room[field]}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneRow() {
|
function cloneRow() {
|
||||||
|
@ -189,6 +214,8 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
const maxParticipantsLimit = props.restriction?.maxParticipants || -1;
|
const maxParticipantsLimit = props.restriction?.maxParticipants || -1;
|
||||||
const minParticipantsLimit = (props.restriction?.maxParticipants || -1) < 1 ? 0 : 1;
|
const minParticipantsLimit = (props.restriction?.maxParticipants || -1) < 1 ? 0 : 1;
|
||||||
|
|
||||||
|
const adminRoom = room.permission === null || room.permission === Permission.Admin;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr className={showRecordings ? 'selected-row' : ''}>
|
<tr className={showRecordings ? 'selected-row' : ''}>
|
||||||
|
@ -214,7 +241,7 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td className="name">
|
<td className="name">
|
||||||
{edit('name')}
|
{edit('name', 'text', adminRoom)}
|
||||||
</td>
|
</td>
|
||||||
<td className="bbb-shrink">
|
<td className="bbb-shrink">
|
||||||
{room.userId !== OC.currentUser && <img src={avatarUrl} alt="Avatar" className="bbb-avatar" />}
|
{room.userId !== OC.currentUser && <img src={avatarUrl} alt="Avatar" className="bbb-avatar" />}
|
||||||
|
@ -224,35 +251,43 @@ const RoomRow: React.FC<Props> = (props) => {
|
||||||
{accessToIcon(room.access)}
|
{accessToIcon(room.access)}
|
||||||
</td>
|
</td>
|
||||||
<td className="max-participants bbb-shrink">
|
<td className="max-participants bbb-shrink">
|
||||||
{edit('maxParticipants', 'number', {min: minParticipantsLimit, max: maxParticipantsLimit < 0 ? undefined : maxParticipantsLimit})}
|
{edit('maxParticipants', 'number', adminRoom, {min: minParticipantsLimit, max: maxParticipantsLimit < 0 ? undefined : maxParticipantsLimit})}
|
||||||
</td>
|
</td>
|
||||||
<td className="record bbb-shrink">
|
<td className="record bbb-shrink">
|
||||||
<input id={'bbb-record-' + room.id} type="checkbox" className="checkbox" disabled={!props.restriction?.allowRecording} checked={room.record} onChange={(event) => updateRoom('record', event.target.checked)} />
|
<input id={'bbb-record-' + room.id} type="checkbox" className="checkbox" disabled={!adminRoom || !props.restriction?.allowRecording} checked={room.record} onChange={(event) => updateRoom('record', event.target.checked)} />
|
||||||
<label htmlFor={'bbb-record-' + room.id}></label>
|
<label htmlFor={'bbb-record-' + room.id}></label>
|
||||||
</td>
|
</td>
|
||||||
<td className="bbb-shrink"><RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} /></td>
|
<td className="bbb-shrink">
|
||||||
<td className="edit icon-col">
|
{<RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} />}
|
||||||
<EditRoom room={props.room} restriction={props.restriction} updateProperty={updateRoom} />
|
|
||||||
</td>
|
</td>
|
||||||
<td className="clone icon-col">
|
<td className="clone icon-col">
|
||||||
|
{adminRoom &&
|
||||||
<button
|
<button
|
||||||
className="action-item"
|
className="action-item"
|
||||||
onClick={cloneRow}
|
onClick={cloneRow}
|
||||||
title={t('bbb', 'Clone room')}>
|
title={t('bbb', 'Clone room')}>
|
||||||
<span className="icon icon-template-add icon-visible"></span>
|
<span className="icon icon-template-add icon-visible"></span>
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td className="edit icon-col">
|
||||||
|
{adminRoom &&
|
||||||
|
<EditRoom room={props.room} restriction={props.restriction} updateProperty={updateRoom} />
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td className="remove icon-col">
|
<td className="remove icon-col">
|
||||||
|
{adminRoom &&
|
||||||
<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>
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{showRecordings && <tr className="recordings-row">
|
{showRecordings && <tr className="recordings-row">
|
||||||
<td colSpan={11}>
|
<td colSpan={11}>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{recordings?.sort((r1, r2) => r1.startTime - r2.startTime).map(recording => <RecordingRow key={recording.id} recording={recording} deleteRecording={deleteRecording} storeRecording={storeRecording} />)}
|
{recordings?.sort((r1, r2) => r1.startTime - r2.startTime).map(recording => <RecordingRow key={recording.id} isAdmin={adminRoom} recording={recording} deleteRecording={deleteRecording} storeRecording={storeRecording} publishRecording={publishRecording} />)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import { api, ShareWith, ShareType, RoomShare, Room, Permission } from '../Common/Api';
|
import { api, ShareWith, ShareType, RoomShare, Room, Permission } from '../Common/Api';
|
||||||
import './ShareWith.scss';
|
import './ShareWith.scss';
|
||||||
import ShareSelection from '../Common/ShareSelection';
|
import ShareSelection from '../Common/ShareSelection';
|
||||||
|
import { PermissionsOptions } from '../Common/Translation';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -45,9 +46,7 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
|
||||||
setShares((allShares ? [...allShares] : []).filter(share => share.id !== id));
|
setShares((allShares ? [...allShares] : []).filter(share => share.id !== id));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleAdminShare(share: RoomShare) {
|
async function setSharePermission(share: RoomShare, newPermission: number) {
|
||||||
const newPermission = share.permission === Permission.Admin ? Permission.Moderator : Permission.Admin;
|
|
||||||
|
|
||||||
return addRoomShare(share.shareWith, share.shareType, share.shareWithDisplayName || share.shareWith, newPermission);
|
return addRoomShare(share.shareWith, share.shareType, share.shareWithDisplayName || share.shareWith, newPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,17 +58,40 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ucFirst(s: string)
|
||||||
|
{
|
||||||
|
return s && s[0].toUpperCase() + s.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPermission(value: Permission, onChange: (value: number) => void) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bbb-form-element bbb-simple-menu">
|
||||||
|
<select name="permission" value={value} onChange={(event) => onChange(Number(event.target.value))}>
|
||||||
|
{Object.keys(PermissionsOptions).map(key => {
|
||||||
|
const label = PermissionsOptions[key];
|
||||||
|
return <option key={key} value={key}>{ucFirst(label)}</option>;
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function permissionLabel(permission: Permission) {
|
||||||
|
return PermissionsOptions[permission] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
function renderShares(shares: RoomShare[]) {
|
function renderShares(shares: RoomShare[]) {
|
||||||
const currentUser = OC.getCurrentUser();
|
const currentUser = OC.getCurrentUser();
|
||||||
|
const ROOM_OWNER_ID = -1;
|
||||||
const ownShare = {
|
const ownShare = {
|
||||||
id: -1,
|
id: ROOM_OWNER_ID,
|
||||||
roomId: room.id,
|
roomId: room.id,
|
||||||
shareType: ShareType.User,
|
shareType: ShareType.User,
|
||||||
shareWith: currentUser.uid,
|
shareWith: currentUser.uid,
|
||||||
shareWithDisplayName: currentUser.displayName,
|
shareWithDisplayName: currentUser.displayName,
|
||||||
permission: Permission.Admin,
|
permission: Permission.Admin,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="bbb-shareWith">
|
<ul className="bbb-shareWith">
|
||||||
{[ownShare, ...shares].map(share => {
|
{[ownShare, ...shares].map(share => {
|
||||||
|
@ -85,20 +107,13 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
|
||||||
</div>
|
</div>
|
||||||
<div className="bbb-shareWith__item__label">
|
<div className="bbb-shareWith__item__label">
|
||||||
<h5>{displayName}
|
<h5>{displayName}
|
||||||
{(share.permission === Permission.Moderator && permission === Permission.User) && (' (' + t('bbb', 'moderator') + ')')}
|
{(share.id === ROOM_OWNER_ID || !isOwner) && (' (' + permissionLabel(share.permission) + ')')}
|
||||||
{(share.permission === Permission.Admin) && (' (' + t('bbb', 'admin') + ')')}</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
{(share.id > -1 && permission === Permission.Moderator && isOwner) && <div className="bbb-shareWith__item__action">
|
{(share.id > ROOM_OWNER_ID && isOwner) && selectPermission(share.permission, (value) => {
|
||||||
<button className="action-item"
|
setSharePermission(share, value);
|
||||||
onClick={ev => {
|
})}
|
||||||
ev.preventDefault();
|
{(share.id > ROOM_OWNER_ID && isOwner) && <div className="bbb-shareWith__item__action">
|
||||||
toggleAdminShare(share);
|
|
||||||
}}
|
|
||||||
title={t('bbb', 'Share')}>
|
|
||||||
<span className={'icon icon-shared icon-visible ' + (share.permission === Permission.Admin ? 'bbb-icon-selected' : 'bbb-icon-unselected')}></span>
|
|
||||||
</button>
|
|
||||||
</div>}
|
|
||||||
{(share.id > -1 && isOwner) && <div className="bbb-shareWith__item__action">
|
|
||||||
<button className="action-item"
|
<button className="action-item"
|
||||||
onClick={ev => {ev.preventDefault(); deleteRoomShare(share.id);}}
|
onClick={ev => {ev.preventDefault(); deleteRoomShare(share.id);}}
|
||||||
title={t('bbb', 'Delete')}>
|
title={t('bbb', 'Delete')}>
|
||||||
|
@ -116,8 +131,6 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{shares ? renderShares(shares) : loading}
|
|
||||||
|
|
||||||
{isOwner ?
|
{isOwner ?
|
||||||
<ShareSelection
|
<ShareSelection
|
||||||
selectShare={(shareOption) => addRoomShare(shareOption.value.shareWith, shareOption.value.shareType, shareOption.label, permission)}
|
selectShare={(shareOption) => addRoomShare(shareOption.value.shareWith, shareOption.value.shareType, shareOption.label, permission)}
|
||||||
|
@ -127,6 +140,8 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
|
||||||
<span className="icon icon-details icon-visible"></span> {t('bbb', 'You are not allowed to change this option, because this room is shared with you.')}
|
<span className="icon icon-details icon-visible"></span> {t('bbb', 'You are not allowed to change this option, because this room is shared with you.')}
|
||||||
</em>
|
</em>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{shares ? renderShares(shares) : loading}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue