Compare commits

...

14 Commits

Author SHA1 Message Date
Sébastien Marinier 76b61beeda
Merge 65f5cd60af into 122cf10f60 2024-07-31 18:33:05 +02:00
Thibaut 122cf10f60
Merge pull request #283 from arawa/fix/261/missing_wordings_in_transifex
fix: 261 wordings that aren't visible in Transifex
2024-07-31 10:34:03 +02:00
Sébastien Marinier 74c32f4e27
Merge branch 'master' into fix/261/missing_wordings_in_transifex 2024-07-31 09:44:42 +02:00
Nextcloud bot be15e93730
Fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2024-07-31 02:19:11 +00:00
Sebastien Marinier f9ee7eed5a fix: 261 wordings that aren't visible in Transifex
Let's try breaking the lines
2024-07-30 14:19:30 +02:00
Sébastien Marinier 65f5cd60af
Merge branch 'master' into feature/share_moderators_and_users 2024-06-28 11:14:11 +02:00
Sebastien Marinier dd48d55419 feat: videos for all users and moderators
Users and moderators recording views
2024-05-29 17:28:49 +02:00
Sebastien Marinier ac2626bc2c feat: videos for all users and moderators
Admin may change the "published" status of a video. users/moderators may only see the published ones.
2024-05-29 17:25:29 +02:00
Sebastien Marinier 9fed698723 feat: sharing rooms with moderators and users
Dialog and permissions management
2024-05-29 17:09:06 +02:00
Sébastien Marinier fbf33378aa
Merge pull request #2 from arawa/task/2916/design_room_list
feat: manage view of rooms for moderators and users
2024-05-29 16:35:48 +02:00
Sebastien Marinier e846404208 feat: manage view of rooms for moderators and users 2024-05-15 17:39:59 +02:00
Sébastien Marinier 081d2b9150
Merge pull request #1 from arawa/task/2919/room_shared_list_for_moderators_and_users
feat: list all shared rooms for users and moderators
2024-05-15 17:28:19 +02:00
Sebastien Marinier 9538c35e79 fix: use querybuilder for user comparaison 2024-05-15 17:26:56 +02:00
Sebastien Marinier 0219cf5df0 feat: list all shared rooms for users and moderators 2024-05-14 11:15:02 +02:00
14 changed files with 209 additions and 61 deletions

View File

@ -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'],

View File

@ -69,6 +69,7 @@ OC.L10N.register(
"This message is shown to all users in the chat area after they joined." : "Diese Nachricht wird allen Benutzern im Chat-Bereich nach ihrem Beitritt angezeigt.", "This message is shown to all users in the chat area after they joined." : "Diese Nachricht wird allen Benutzern im Chat-Bereich nach ihrem Beitritt angezeigt.",
"Sets a limit on the number of participants for this room. Zero means there is no limit." : "Legt eine Begrenzung der Teilnehmerzahl für diesen Raum fest. Null bedeutet, dass es keine Begrenzung gibt.", "Sets a limit on the number of participants for this room. Zero means there is no limit." : "Legt eine Begrenzung der Teilnehmerzahl für diesen Raum fest. Null bedeutet, dass es keine Begrenzung gibt.",
"If enabled, the moderator is able to start the recording." : "Wenn aktiviert, kann der Moderator die Aufnahme starten.", "If enabled, the moderator is able to start the recording." : "Wenn aktiviert, kann der Moderator die Aufnahme starten.",
"Explanation of the different concepts that constitute access options :<br>- Public: Anyone who has the link can join.- <br>Internal: Only Nextcloud users can join.- <br>Password: Only guests who have the password can join..- <br>Waiting room: A moderator must accept each guest before they can join.- <br>Restricted : Only selected users and groups can access this room." : "Erläuterung der verschiedenen Konzepte bezüglich der Zugriffsrechte:<br>- Öffentlich: Jeder, der den Link hat, kann beitreten. <br>- Intern: Nur Nextcloud-Benutzer können beitreten. <br>- Passwort: Nur Gäste, die das Passwort haben, können beitreten. <br>- Warteraum: Ein Moderator muss jeden Gast vor dem Beitreten bestätigen.<br> Eingeschränkt: Nur ausgewählte Benutzer und Gruppen haben Zugriff auf diesen Raum.",
"A moderator is able to manage all participants in a meeting including kicking, muting or selecting a presenter. Users with the role moderator are also able to close a meeting or change the default settings." : "Ein Moderator kann die Teilnehmer einer Besprechung verwalten, was das Ausschließen, Stummschalten oder Gewährung von Moderatorenrechten beinhaltet. Benutzer mit Moderatorenrechten können auch die Besprechung schließen oder die Standardeinstellungen ändern.", "A moderator is able to manage all participants in a meeting including kicking, muting or selecting a presenter. Users with the role moderator are also able to close a meeting or change the default settings." : "Ein Moderator kann die Teilnehmer einer Besprechung verwalten, was das Ausschließen, Stummschalten oder Gewährung von Moderatorenrechten beinhaltet. Benutzer mit Moderatorenrechten können auch die Besprechung schließen oder die Standardeinstellungen ändern.",
"If enabled, normal users have to wait until a moderator is in the room." : "Wenn aktiviert müssen Benutzer warten, bis ein Moderator den Raum betritt.", "If enabled, normal users have to wait until a moderator is in the room." : "Wenn aktiviert müssen Benutzer warten, bis ein Moderator den Raum betritt.",
"If enabled, a moderator URL is generated which allows access with moderator permission." : "Wenn aktiviert, wird eine Moderator-URL generiert, die den Zugriff mit Moderatorberechtigung ermöglicht.", "If enabled, a moderator URL is generated which allows access with moderator permission." : "Wenn aktiviert, wird eine Moderator-URL generiert, die den Zugriff mit Moderatorberechtigung ermöglicht.",

View File

@ -67,6 +67,7 @@
"This message is shown to all users in the chat area after they joined." : "Diese Nachricht wird allen Benutzern im Chat-Bereich nach ihrem Beitritt angezeigt.", "This message is shown to all users in the chat area after they joined." : "Diese Nachricht wird allen Benutzern im Chat-Bereich nach ihrem Beitritt angezeigt.",
"Sets a limit on the number of participants for this room. Zero means there is no limit." : "Legt eine Begrenzung der Teilnehmerzahl für diesen Raum fest. Null bedeutet, dass es keine Begrenzung gibt.", "Sets a limit on the number of participants for this room. Zero means there is no limit." : "Legt eine Begrenzung der Teilnehmerzahl für diesen Raum fest. Null bedeutet, dass es keine Begrenzung gibt.",
"If enabled, the moderator is able to start the recording." : "Wenn aktiviert, kann der Moderator die Aufnahme starten.", "If enabled, the moderator is able to start the recording." : "Wenn aktiviert, kann der Moderator die Aufnahme starten.",
"Explanation of the different concepts that constitute access options :<br>- Public: Anyone who has the link can join.- <br>Internal: Only Nextcloud users can join.- <br>Password: Only guests who have the password can join..- <br>Waiting room: A moderator must accept each guest before they can join.- <br>Restricted : Only selected users and groups can access this room." : "Erläuterung der verschiedenen Konzepte bezüglich der Zugriffsrechte:<br>- Öffentlich: Jeder, der den Link hat, kann beitreten. <br>- Intern: Nur Nextcloud-Benutzer können beitreten. <br>- Passwort: Nur Gäste, die das Passwort haben, können beitreten. <br>- Warteraum: Ein Moderator muss jeden Gast vor dem Beitreten bestätigen.<br> Eingeschränkt: Nur ausgewählte Benutzer und Gruppen haben Zugriff auf diesen Raum.",
"A moderator is able to manage all participants in a meeting including kicking, muting or selecting a presenter. Users with the role moderator are also able to close a meeting or change the default settings." : "Ein Moderator kann die Teilnehmer einer Besprechung verwalten, was das Ausschließen, Stummschalten oder Gewährung von Moderatorenrechten beinhaltet. Benutzer mit Moderatorenrechten können auch die Besprechung schließen oder die Standardeinstellungen ändern.", "A moderator is able to manage all participants in a meeting including kicking, muting or selecting a presenter. Users with the role moderator are also able to close a meeting or change the default settings." : "Ein Moderator kann die Teilnehmer einer Besprechung verwalten, was das Ausschließen, Stummschalten oder Gewährung von Moderatorenrechten beinhaltet. Benutzer mit Moderatorenrechten können auch die Besprechung schließen oder die Standardeinstellungen ändern.",
"If enabled, normal users have to wait until a moderator is in the room." : "Wenn aktiviert müssen Benutzer warten, bis ein Moderator den Raum betritt.", "If enabled, normal users have to wait until a moderator is in the room." : "Wenn aktiviert müssen Benutzer warten, bis ein Moderator den Raum betritt.",
"If enabled, a moderator URL is generated which allows access with moderator permission." : "Wenn aktiviert, wird eine Moderator-URL generiert, die den Zugriff mit Moderatorberechtigung ermöglicht.", "If enabled, a moderator URL is generated which allows access with moderator permission." : "Wenn aktiviert, wird eine Moderator-URL generiert, die den Zugriff mit Moderatorberechtigung ermöglicht.",

View File

@ -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)[]
* *

View File

@ -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);

View File

@ -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),

View File

@ -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))
) )

View File

@ -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`;

View File

@ -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'),
};

View File

@ -282,6 +282,10 @@ pre {
} }
} }
.bbb-simple-menu {
min-width: auto;
}
.bbb-input-container { .bbb-input-container {
display: flex; display: flex;
} }

View File

@ -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}`}

View File

@ -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">
{isAdmin &&
<button className="action-item" onClick={() => deleteRecording(recording)} title={t('bbb', 'Delete')}> <button className="action-item" onClick={() => deleteRecording(recording)} 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>
); );

View File

@ -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,11 +214,17 @@ 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' : ''}>
<td className="start"> <td className="start">
<a href={api.getRoomUrl(room)} className={`button ${room.running ? 'success' : 'primary'}`} 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')}>
{room.running ? t('bbb', 'Join') : t('bbb', 'Start')} {room.running ? t('bbb', 'Join') : t('bbb', 'Start')}
</a> </a>
</td> </td>
@ -210,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" />}
@ -220,32 +251,48 @@ 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>
{adminRoom &&
<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={!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="edit icon-col"> {!adminRoom &&
<EditRoom room={props.room} restriction={props.restriction} updateProperty={updateRoom} /> <td className="record bbb-shrink">
<span className={'icon '+(room.record ? 'icon-checkmark' : 'icon-close')+' icon-visible'}></span>
</td>
}
<td className="bbb-shrink">
{<RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} />}
</td> </td>
<td className="clone icon-col"> <td className="clone icon-col">
<button className="action-item" onClick={cloneRow} title={t('bbb', 'Clone Room')}> <button
className="action-item"
onClick={cloneRow}
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>
<td className="edit icon-col">
{adminRoom &&
<EditRoom room={props.room} restriction={props.restriction} updateProperty={updateRoom} />
}
</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>

View File

@ -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}
</> </>
); );
}; };