Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⏳ Lobby #1926

Merged
merged 27 commits into from
Aug 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
12952b9
Add a lobby state column to the room table
nickvergessen Jun 27, 2019
a48650c
Read and allow to set the lobby state
nickvergessen Jun 27, 2019
8524f18
Send notification to signaling backend when lobby state changes
danxuliu Aug 26, 2019
d8563f8
Add an API endpoint to set the lobby state
nickvergessen Jun 27, 2019
3b205bf
Add support for promoting and demoting guests in the integration tests
danxuliu Aug 26, 2019
d59725e
Add integration tests for setting the lobby state
danxuliu Aug 26, 2019
6b6f42d
Prevent access to some APIs for non-moderators if lobby is enabled
nickvergessen Jun 27, 2019
ea48d5a
Add integration tests for preventing access to some APIs in the lobby
danxuliu Aug 26, 2019
7f0dd77
Allow users to load the room data when joining it
nickvergessen Jul 8, 2019
36e3da0
No participants and chat messages for users in the lobby
nickvergessen Jul 4, 2019
8cd5064
Allow a timer as well
nickvergessen Jul 4, 2019
ac1c152
Add capability
nickvergessen Jul 5, 2019
3eec1b0
Use a different status code for lobby blocked requests
nickvergessen Jul 8, 2019
d4b7300
Remove outdated comment
nickvergessen Jul 8, 2019
42f8b06
Add system messages for the lobby state change
nickvergessen Jul 9, 2019
18336c0
Transform time to UTC before storing in the database
nickvergessen Jul 23, 2019
f56ab8f
Prevent nested calls between setLobby and getLobbyState when the time…
nickvergessen Jul 23, 2019
1136b70
Fix the system message when the timer was reached
nickvergessen Jul 24, 2019
369c3e4
Make the API work with timestamps
nickvergessen Jul 29, 2019
0e4fcbb
Bump version to trigger the migration
danxuliu Aug 26, 2019
8745fd1
Rename lobby constants
danxuliu Aug 28, 2019
787440f
Extract methods to set the main view
danxuliu Mar 4, 2019
3a5549c
Add UI to set the lobby state
danxuliu Aug 26, 2019
0ab60ec
Show empty content view when current participant is in the lobby
danxuliu Aug 27, 2019
bd56d8c
Show the start time in the lobby message of the empty content view
danxuliu Aug 27, 2019
c61047a
Add input to set the lobby timer
danxuliu Aug 27, 2019
22be1a9
Add acceptance tests for joining a room with a lobby
danxuliu Aug 28, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m

]]></description>

<version>7.0.0-dev.2</version>
<version>7.0.0-dev.3</version>
<licence>agpl</licence>

<author>Daniel Calviño Sánchez</author>
Expand Down
13 changes: 13 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,19 @@
'id' => '^\d+$',
],
],

/**
* Webinary
*/
[
'name' => 'Webinary#setLobby',
'url' => '/api/{apiVersion}/room/{token}/webinary/lobby',
'verb' => 'PUT',
'requirements' => [
'apiVersion' => 'v1',
'token' => '^[a-z0-9]{4,30}$',
],
],
],
];

39 changes: 33 additions & 6 deletions css/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ input[type="password"] {
padding-left: 1px;
}

.room-moderation-button .menu li div.separator,
#app-navigation .app-navigation-entry-menu li div.separator {
border-bottom: 1px solid var(--color-border-dark);
margin: 0 10px;
Expand Down Expand Up @@ -1049,23 +1050,48 @@ body:not(#body-public) .participantWithList > li > span:not(.currentUser):not(.g
}
}

.menuitem.password-option {
li {
> .separator {

margin-top: $outter-margin - 2px; // minus the input margin
}
}

.menuitem.caption,
.menuitem.caption > span {
/* Override rule for menu items from server, as in this case the
* caption is not clickable. */
cursor: default;
}

.menuitem.caption {
font-weight: bold;

&:hover,
&:focus,
&:active {
opacity: 0.7 !important;
}
}

.menuitem.password-option,
.menuitem.lobby-timer-option {
/* Override rule for menu items from server, as in this case
* only the button in the password field is clickable, so the
* pointer cursor should not be used for the whole item. */
cursor: default;

.password-form {
form {
position: relative;

.password-confirm,
.password-loading {
.icon-confirm,
.icon-loading-small {
/* Inputs in menu items do not have a right margin, so
* it does not need to be compensated. */
right: 0;
}

.password-confirm {
.icon-confirm {
/* Needed to override an important rule set in the
* server. */
background-color: transparent !important;
Expand All @@ -1076,7 +1102,8 @@ body:not(#body-public) .participantWithList > li > span:not(.currentUser):not(.g

/* The specific locator is needed to have higher priority than other
* important rules set in the server. */
input.checkbox + label.link-checkbox-label {
input.checkbox + label.link-checkbox-label,
input.radio + label {
/* If ".icon-loading-small" is set hide the checkbox and show a
* loading icon instead. */
&.icon-loading-small:before {
Expand Down
2 changes: 1 addition & 1 deletion docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ title: Capabilities
* `locked-one-to-one-rooms` - One-to-one conversations are now locked to the users. Neither guests nor other participants can be added, so the options to do that should be hidden as well. Also a user can only leave a one-to-one conversation (not delete). It will be deleted when the other participant left too. If the other participant posts a new chat message or starts a call, the left-participant will be re-added.
* `read-only-rooms` - Conversations can be in `read-only` mode which means people can not do calls or write chat messages.


## 7.0
* `chat-read-marker` - The chat can be optionally marked read by clients manually, independent from the loading of the chat messages.
* `webinary-lobby` - See [Webinary management](webinary.md) for technical details.
4 changes: 4 additions & 0 deletions docs/constants.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ title: Constants
* `guests` - guest users
* `users` - logged-in users
* `bots` - used by commands (actor-id is the used `/command`) and the changelog conversation (actor-id is `changelog`)

## Webinary lobby states
* `0` no lobby
* `1` lobby for non moderators
2 changes: 2 additions & 0 deletions docs/conversation.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
`lastActivity` | int | Timestamp of the last activity in the conversation, in seconds and UTC time zone
`isFavorite` | bool | Flag if the conversation is favorited by the user
`notificationLevel` | int | The notification level for the user (one of `Participant::NOTIFY_*` (1-3))
`lobbyState` | int | Webinary lobby restriction (0-1), if the participant is a moderator they can always join the conversation (only available with `webinary-lobby` capability)
`lobbyTimer` | int | Timestamp when the lobby will be automatically disabled (only available with `webinary-lobby` capability)
`unreadMessages` | int | Number of unread chat messages in the conversation (only available with `chat-v2` capability)
`unreadMention` | bool | Flag if the user was mentioned since their last visit
`lastReadMessage` | int | ID of the last read message in a room (only available with `chat-read-marker` capability)
Expand Down
28 changes: 28 additions & 0 deletions docs/webinary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Webinary management

Group and public conversations can be used to host webinaries. Those online meetings can have a lobby, which come with the following restrictions:
* Only moderators can start/join a call
* Only moderators can read and write chat messages
* Normal users can only join the room. They then pull the room endpoint regularly for an update and should start the chat and signaling as well as allowing to join the call, once the lobby got disabled.


Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`

## Set lobby for a conversation

* Required capability: `webinary-lobby`
* Method: `PUT`
* Endpoint: `/room/{token}/webinary/lobby`
* Data:

field | type | Description
------|------|------------
`state` | int | New state for the conversation
`timer` | int/null | Timestamp when the lobby state is reset to no lobby

* Response:
- Header:
+ `200 OK`
+ `400 Bad Request` When the conversation type does not support lobby (only group and public conversation atm)
+ `403 Forbidden` When the current user is not a moderator/owner
+ `404 Not Found` When the conversation could not be found for the participant
89 changes: 64 additions & 25 deletions js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
ROOM_TYPE_PUBLIC: 3,
ROOM_TYPE_CHANGELOG: 4,

/* Must stay in sync with values in "lib/Webinary.php". */
LOBBY_NONE: 0,
LOBBY_NON_MODERATORS: 1,

/** @property {OCA.SpreedMe.Models.Room} activeRoom */
activeRoom: null,

Expand Down Expand Up @@ -355,7 +359,9 @@
this.signaling.syncRooms()
.then(function() {
self.stopListening(self.activeRoom, 'change:displayName');
self.stopListening(self.activeRoom, 'change:participantType');
self.stopListening(self.activeRoom, 'change:participantFlags');
self.stopListening(self.activeRoom, 'change:lobbyState');

if (OC.getCurrentUser().uid) {
roomChannel.trigger('active', token);
Expand All @@ -376,6 +382,10 @@

self.updateContentsLayout();
self.listenTo(self.activeRoom, 'change:participantFlags', self.updateContentsLayout);
self.listenTo(self.activeRoom, 'change:participantType', self.updateContentsLayout);
self.listenTo(self.activeRoom, 'change:participantType', self._updateSidebar);
self.listenTo(self.activeRoom, 'change:lobbyState', self.updateContentsLayout);
self.listenTo(self.activeRoom, 'change:lobbyState', self._updateSidebar);

self.updateSidebarWithActiveRoom();
});
Expand All @@ -386,16 +396,22 @@
return;
}

if (this.activeRoom.isCurrentParticipantInLobby()) {
this._showEmptyContentViewInMainView();

return;
}

var flags = this.activeRoom.get('participantFlags') || 0;
var inCall = flags & OCA.SpreedMe.app.FLAG_IN_CALL !== 0;
if (inCall && this._chatViewInMainView === true) {
this._chatView.$el.detach();
this._sidebarView.addTab('chat', { label: t('spreed', 'Chat'), icon: 'icon-comment', priority: 100 }, this._chatView);
this._sidebarView.selectTab('chat');
this._chatView.reloadMessageList();
this._chatView.setTooltipContainer(this._chatView.$el);
this._chatViewInMainView = false;
} else if (!inCall && !this._chatViewInMainView) {
if (inCall) {
this._showCallViewInMainView();
} else if (!inCall) {
this._showChatViewInMainView();
}
},
_showChatViewInMainView: function() {
if (!this._chatViewInMainView) {
this._sidebarView.removeTab('chat');
this._chatView.$el.prependTo('#app-content-wrapper');
this._chatView.reloadMessageList();
Expand All @@ -404,18 +420,38 @@
this._chatViewInMainView = true;
}

if (inCall) {
$('#videos').show();
$('#screens').show();
$('#emptycontent').hide();
} else {
$('#videos').hide();
$('#screens').hide();
$('#emptycontent').show();
$('#videos').hide();
$('#screens').hide();
$('#emptycontent').show();
},
_showCallViewInMainView: function() {
if (this._chatViewInMainView) {
this._chatView.$el.detach();
this._sidebarView.addTab('chat', { label: t('spreed', 'Chat'), icon: 'icon-comment', priority: 100 }, this._chatView);
this._sidebarView.selectTab('chat');
this._chatView.reloadMessageList();
this._chatView.setTooltipContainer(this._chatView.$el);
this._chatViewInMainView = false;
}

$('#videos').show();
$('#screens').show();
$('#emptycontent').hide();
},
updateSidebarWithActiveRoom: function() {
this._sidebarView.enable();
_showEmptyContentViewInMainView: function() {
this._chatView.$el.detach();
this._chatViewInMainView = false;

$('#videos').hide();
$('#screens').hide();
$('#emptycontent').show();
},
_updateSidebar: function() {
if (!this.activeRoom.isCurrentParticipantInLobby()) {
this._sidebarView.enable();
} else {
this._sidebarView.disable();
}

// The sidebar has a width of 27% of the window width and a minimum
// width of 300px. Therefore, when the window is 1111px wide or
Expand All @@ -429,6 +465,13 @@
this._sidebarView.open();
}

if (this.activeRoom.isCurrentParticipantInLobby()) {
this._messageCollection.stopReceivingMessages();
} else {
this._messageCollection.receiveMessages();
}
},
updateSidebarWithActiveRoom: function() {
var callInfoView = new OCA.SpreedMe.Views.CallInfoView({
model: this.activeRoom,
guestNameModel: this._localStorageModel
Expand All @@ -437,7 +480,8 @@

this._chatView.setRoom(this.activeRoom);
this._messageCollection.setRoomToken(this.activeRoom.get('token'));
this._messageCollection.receiveMessages();

this._updateSidebar();
},
setPageTitle: function(title){
if (title) {
Expand Down Expand Up @@ -574,12 +618,7 @@
});

this.listenTo(roomChannel, 'leaveCurrentRoom', function() {
this._chatView.$el.detach();
this._chatViewInMainView = false;

$('#videos').hide();
$('#screens').hide();
$('#emptycontent').show();
this._showEmptyContentViewInMainView();
});

this.listenTo(roomChannel, 'joinRoom', function(token) {
Expand Down
50 changes: 50 additions & 0 deletions js/models/room.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
unreadMention: false,
isFavorite: false,
notificationLevel: 0,
lobbyState: 0,
lobbyTimer: 0,
lastPing: 0,
sessionId: '0',
participants: [],
Expand Down Expand Up @@ -106,13 +108,19 @@
return 'Public room type can only be changed to group';
}
}

if (attributes.lobbyTimer && this.attributes.lobbyState !== OCA.SpreedMe.app.LOBBY_NON_MODERATORS) {
return 'Lobby timer can be set only when lobby state is non moderators';
}
},
save: function(key, value, options) {
if (typeof key !== 'string') {
throw 'Room.save only supports single attributes';
}

var supportedKeys = [
'lobbyState',
'lobbyTimer',
'name',
'password',
'type',
Expand Down Expand Up @@ -200,6 +208,31 @@
options.url = this.url() + '/password';
}

if (method === 'patch' && options.attrs.lobbyState !== undefined) {
method = 'update';

options.url = this.url() + '/webinary/lobby';

// The endpoint to set the lobby state expects the state to be
// provided in a "state" attribute instead of a "lobbyState"
// attribute.
options.attrs.state = options.attrs.lobbyState;
delete options.attrs.lobbyState;
}

if (method === 'patch' && options.attrs.lobbyTimer !== undefined) {
method = 'update';

options.url = this.url() + '/webinary/lobby';

// The endpoint to set the lobby state expects the state and
// timer to be provided in "state" and "timer" attribute instead
// of "lobbyState" and "lobbyTimer" attributes.
options.attrs.state = this.attributes.lobbyState;
options.attrs.timer = options.attrs.lobbyTimer;
delete options.attrs.lobbyTimer;
}

return Backbone.Model.prototype.sync.call(this, method, model, options);
},
setPublic: function(isPublic, options) {
Expand All @@ -210,6 +243,12 @@
setPassword: function(password, options) {
this.save('password', password, options);
},
setLobbyState: function(lobbyState, options) {
this.save('lobbyState', lobbyState, options);
},
setLobbyTimer: function(lobbyTimer, options) {
this.save('lobbyTimer', lobbyTimer, options);
},
join: function() {
OCA.SpreedMe.app.connection.joinRoom(this.get('token'));
},
Expand Down Expand Up @@ -247,6 +286,17 @@

return Backbone.Model.prototype.destroy.call(this, options);
},
isCurrentParticipantInLobby: function() {
var isModerator = this.get('participantType') !== OCA.SpreedMe.app.USER &&
this.get('participantType') !== OCA.SpreedMe.app.USERSELFJOINED &&
this.get('participantType') !== OCA.SpreedMe.app.GUEST;

if (this.get('lobbyState') === OCA.SpreedMe.app.LOBBY_NON_MODERATORS && !isModerator) {
return true;
}

return false;
},
});

OCA.SpreedMe.Models.Room = Room;
Expand Down
Loading