Skip to content

Commit

Permalink
Merge pull request #1926 from nextcloud/feature/382/lobby
Browse files Browse the repository at this point in the history
⏳ Lobby
  • Loading branch information
nickvergessen authored Aug 28, 2019
2 parents b2e30c4 + 22be1a9 commit 6f1e4be
Show file tree
Hide file tree
Showing 35 changed files with 1,232 additions and 51 deletions.
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

0 comments on commit 6f1e4be

Please sign in to comment.