Skip to content

Commit 83e7a39

Browse files
authored
Improve copy recording functionality (#6154)
1 parent 82c04e1 commit 83e7a39

6 files changed

Lines changed: 136 additions & 58 deletions

File tree

app/assets/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@
436436
"recording_visibility_updated": "The recording visibility has been updated.",
437437
"recording_name_updated": "The recording name has been updated.",
438438
"recording_deleted": "The recording has been deleted.",
439-
"copied_urls": "The recording URLs have been copied."
439+
"copied_urls": "The recording URL has been copied."
440440
},
441441
"role": {
442442
"role_created": "A new role has been created.",

app/assets/stylesheets/recordings.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,17 @@
7676
}
7777
}
7878

79-
.recordings-list {
79+
.recordings-list, .popover-body {
8080
.recording-icon-circle {
8181
min-width: 40px;
8282
min-height: 40px;
8383
background-color: var(--brand-color-light);
8484
}
8585

86+
[class*="btn-format-"]:hover {
87+
background-image: linear-gradient(rgb(0 0 0/5%) 0 0);
88+
}
89+
8690
.btn-format-presentation {
8791
background-color: #f8f5fe;
8892
color: #6941c6;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
2+
//
3+
// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
4+
//
5+
// This program is free software; you can redistribute it and/or modify it under the
6+
// terms of the GNU Lesser General Public License as published by the Free Software
7+
// Foundation; either version 3.0 of the License, or (at your option) any later
8+
// version.
9+
//
10+
// Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
11+
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
12+
// PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License along
15+
// with Greenlight; if not, see <http://www.gnu.org/licenses/>.
16+
17+
/* eslint-disable react/jsx-props-no-spreading */
18+
19+
import React, { forwardRef } from 'react';
20+
import { useTranslation } from 'react-i18next';
21+
import Popover from 'react-bootstrap/Popover';
22+
import PropTypes from 'prop-types';
23+
import { Button } from 'react-bootstrap';
24+
import useCopyRecordingUrl from '../../hooks/mutations/recordings/useCopyRecordingUrl';
25+
26+
const CopyRecordingPopover = forwardRef(({
27+
recording, formats, onCopied, ...popoverProps
28+
}, ref) => {
29+
const { t } = useTranslation();
30+
const copyRecordingUrl = useCopyRecordingUrl();
31+
32+
return (
33+
<Popover id="popover-basic" ref={ref} {...popoverProps}>
34+
<Popover.Header as="h3">{t('recording.copy_recording_urls')}</Popover.Header>
35+
<Popover.Body>
36+
{recording?.visibility !== 'Unpublished' && formats?.map((format) => (
37+
<Button
38+
onClick={() => copyRecordingUrl.mutate(
39+
{ record_id: recording.record_id, format: format.recording_type },
40+
{ onSuccess: () => onCopied() },
41+
)}
42+
className={`btn-sm rounded-pill me-1 mt-1 border-0 btn-format-${format.recording_type.toLowerCase()}`}
43+
key={`${format.recording_type}-${format.url}`}
44+
>
45+
{format.recording_type}
46+
</Button>
47+
))}
48+
</Popover.Body>
49+
</Popover>
50+
);
51+
});
52+
53+
export default CopyRecordingPopover;
54+
55+
CopyRecordingPopover.propTypes = {
56+
recording: PropTypes.shape({
57+
id: PropTypes.string.isRequired,
58+
record_id: PropTypes.string.isRequired,
59+
name: PropTypes.string.isRequired,
60+
length: PropTypes.number.isRequired,
61+
participants: PropTypes.number.isRequired,
62+
formats: PropTypes.arrayOf(PropTypes.shape({
63+
url: PropTypes.string.isRequired,
64+
recording_type: PropTypes.string.isRequired,
65+
})),
66+
visibility: PropTypes.string.isRequired,
67+
protectable: PropTypes.bool,
68+
recorded_at: PropTypes.string.isRequired,
69+
map: PropTypes.func,
70+
user_name: PropTypes.string,
71+
}).isRequired,
72+
formats: PropTypes.arrayOf(PropTypes.shape({
73+
url: PropTypes.string.isRequired,
74+
recording_type: PropTypes.string.isRequired,
75+
})).isRequired,
76+
onCopied: PropTypes.func.isRequired,
77+
};

app/javascript/components/recordings/RecordingRow.jsx

Lines changed: 31 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,24 @@
1515
// with Greenlight; if not, see <http://www.gnu.org/licenses/>.
1616

1717
import {
18-
VideoCameraIcon, TrashIcon, PencilSquareIcon, ClipboardDocumentIcon, EllipsisVerticalIcon,
18+
VideoCameraIcon, TrashIcon, PencilSquareIcon, ClipboardDocumentIcon,
1919
} from '@heroicons/react/24/outline';
2020
import React, { useState } from 'react';
2121
import PropTypes from 'prop-types';
2222
import {
2323
Button, Stack, Dropdown,
2424
} from 'react-bootstrap';
2525
import { useTranslation } from 'react-i18next';
26+
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
2627
import { useAuth } from '../../contexts/auth/AuthProvider';
2728
import Spinner from '../shared_components/utilities/Spinner';
2829
import UpdateRecordingForm from './forms/UpdateRecordingForm';
2930
import DeleteRecordingForm from './forms/DeleteRecordingForm';
3031
import Modal from '../shared_components/modals/Modal';
3132
import { localizeDateTimeString } from '../../helpers/DateTimeHelper';
3233
import useRedirectRecordingUrl from '../../hooks/mutations/recordings/useRedirectRecordingUrl';
33-
import useCopyRecordingUrl from '../../hooks/mutations/recordings/useCopyRecordingUrl';
3434
import SimpleSelect from '../shared_components/utilities/SimpleSelect';
35+
import CopyRecordingPopover from './CopyRecordingPopover';
3536

3637
// TODO: Amir - Refactor this.
3738
export default function RecordingRow({
@@ -43,10 +44,10 @@ export default function RecordingRow({
4344
const [isEditing, setIsEditing] = useState(false);
4445
const [isUpdating, setIsUpdating] = useState(false);
4546
const [display, setDisplay] = useState('invisible');
47+
const [showCopyPopover, setShowCopyPopover] = useState(false);
4648

4749
const currentUser = useAuth();
4850
const redirectRecordingUrl = useRedirectRecordingUrl();
49-
const copyRecordingUrl = useCopyRecordingUrl();
5051
const allowedVisibilities = JSON.parse(currentUser.permissions?.AccessToVisibilities);
5152

5253
const localizedTime = localizeDateTimeString(recording?.recorded_at, currentUser?.language);
@@ -170,50 +171,36 @@ export default function RecordingRow({
170171
))}
171172
</td>
172173
<td className="border-start-0">
173-
{adminTable
174-
? (
175-
<Dropdown className="float-end cursor-pointer">
176-
<Dropdown.Toggle className="hi-s" as={EllipsisVerticalIcon} />
177-
<Dropdown.Menu>
178-
<Dropdown.Item onClick={() => copyRecordingUrl.mutate({ record_id: recording.record_id })}>
179-
<ClipboardDocumentIcon className="hi-s me-2" />
180-
{t('recording.copy_recording_urls')}
181-
</Dropdown.Item>
182-
<Modal
183-
modalButton={<Dropdown.Item><TrashIcon className="hi-s me-2" />{t('delete')}</Dropdown.Item>}
184-
body={(
185-
<DeleteRecordingForm
186-
mutation={useDeleteAPI}
187-
recordId={recording.record_id}
188-
/>
189-
)}
174+
<Stack direction="horizontal" className="float-end recordings-icons">
175+
{ recording?.visibility !== 'Unpublished' && (
176+
<OverlayTrigger
177+
trigger="click"
178+
show={showCopyPopover}
179+
onToggle={(show) => setShowCopyPopover(show)}
180+
rootClose
181+
overlay={(
182+
<CopyRecordingPopover
183+
recording={recording}
184+
formats={formats}
185+
onCopied={() => setShowCopyPopover(false)}
190186
/>
191-
</Dropdown.Menu>
192-
</Dropdown>
193-
)
194-
: (
195-
<Stack direction="horizontal" className="float-end recordings-icons">
196-
{ recording?.visibility !== 'Unpublished' && (
197-
<Button
198-
variant="icon"
199-
className="mt-1 me-3"
200-
title={t('recording.copy_recording_urls')}
201-
onClick={() => copyRecordingUrl.mutate({ record_id: recording.record_id })}
202-
>
203-
<ClipboardDocumentIcon className="hi-s text-muted" />
204-
</Button>
205187
)}
206-
<Modal
207-
modalButton={<Dropdown.Item className="btn btn-icon"><TrashIcon className="hi-s me-2" title={t('delete')} /></Dropdown.Item>}
208-
body={(
209-
<DeleteRecordingForm
210-
mutation={useDeleteAPI}
211-
recordId={recording.record_id}
212-
/>
213-
)}
214-
/>
215-
</Stack>
188+
>
189+
<Button variant="icon" className="mt-1 me-3" title={t('recording.copy_recording_urls')}>
190+
<ClipboardDocumentIcon className="hi-s text-muted" />
191+
</Button>
192+
</OverlayTrigger>
216193
)}
194+
<Modal
195+
modalButton={<Dropdown.Item className="btn btn-icon"><TrashIcon className="hi-s me-2" title={t('delete')} /></Dropdown.Item>}
196+
body={(
197+
<DeleteRecordingForm
198+
mutation={useDeleteAPI}
199+
recordId={recording.record_id}
200+
/>
201+
)}
202+
/>
203+
</Stack>
217204
</td>
218205
</tr>
219206
);

app/javascript/components/rooms/room/public_recordings/PublicRecordingRow.jsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// You should have received a copy of the GNU Lesser General Public License along
1515
// with Greenlight; if not, see <http://www.gnu.org/licenses/>.
1616

17-
import React from 'react';
17+
import React, { useState } from 'react';
1818
import {
1919
VideoCameraIcon, ClipboardDocumentIcon,
2020
} from '@heroicons/react/24/outline';
@@ -23,10 +23,11 @@ import {
2323
Button, Stack,
2424
} from 'react-bootstrap';
2525
import { useTranslation } from 'react-i18next';
26+
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
2627
import { useAuth } from '../../../../contexts/auth/AuthProvider';
2728
import { localizeDateTimeString } from '../../../../helpers/DateTimeHelper';
2829
import useRedirectRecordingUrl from '../../../../hooks/mutations/recordings/useRedirectRecordingUrl';
29-
import useCopyRecordingUrl from '../../../../hooks/mutations/recordings/useCopyRecordingUrl';
30+
import CopyRecordingPopover from '../../../recordings/CopyRecordingPopover';
3031

3132
// TODO: Amir - Refactor this.
3233
export default function PublicRecordingRow({
@@ -36,7 +37,7 @@ export default function PublicRecordingRow({
3637

3738
const currentUser = useAuth();
3839
const redirectRecordingUrl = useRedirectRecordingUrl();
39-
const copyRecordingUrl = useCopyRecordingUrl();
40+
const [showCopyPopover, setShowCopyPopover] = useState(false);
4041

4142
const localizedTime = localizeDateTimeString(recording?.recorded_at, currentUser?.language);
4243
const formats = recording.formats.sort(
@@ -70,14 +71,23 @@ export default function PublicRecordingRow({
7071
</td>
7172
<td className="border-start-0">
7273
<Stack direction="horizontal" className="float-end recordings-icons">
73-
<Button
74-
variant="icon"
75-
className="mt-1 me-3"
76-
title={t('recording.copy_recording_urls')}
77-
onClick={() => copyRecordingUrl.mutate({ record_id: recording.record_id })}
74+
<OverlayTrigger
75+
trigger="click"
76+
show={showCopyPopover}
77+
onToggle={(show) => setShowCopyPopover(show)}
78+
rootClose
79+
overlay={(
80+
<CopyRecordingPopover
81+
recording={recording}
82+
formats={formats}
83+
onCopied={() => setShowCopyPopover(false)}
84+
/>
85+
)}
7886
>
79-
<ClipboardDocumentIcon className="hi-s text-muted" />
80-
</Button>
87+
<Button variant="icon" className="mt-1 me-3" title={t('recording.copy_recording_urls')}>
88+
<ClipboardDocumentIcon className="hi-s text-muted" />
89+
</Button>
90+
</OverlayTrigger>
8191
</Stack>
8292
</td>
8393
</tr>

app/javascript/hooks/mutations/recordings/useCopyRecordingUrl.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ export default function useCopyRecordingUrl() {
2323
const { t } = useTranslation();
2424

2525
return useMutation(
26-
(data) => axios.post('/recordings/recording_url.json', { id: data.record_id })
26+
(data) => axios.post('/recordings/recording_url.json', { id: data.record_id, recording_format: data.format })
2727
.then((resp) => resp.data),
2828
{
2929
onSuccess: (url) => {
30-
navigator.clipboard.writeText(url?.join('\n')).then(() => toast.success(t('toast.success.recording.copied_urls')));
30+
navigator.clipboard.writeText(url?.data).then(() => toast.success(t('toast.success.recording.copied_urls')));
3131
},
3232
onError: () => {
3333
toast.error(t('toast.error.problem_completing_action'));

0 commit comments

Comments
 (0)