diff --git a/packages/rocketchat-lib/client/lib/RocketChatAnnouncement.js b/packages/rocketchat-lib/client/lib/RocketChatAnnouncement.js
index 42c5a45f30dd..07950144cd18 100644
--- a/packages/rocketchat-lib/client/lib/RocketChatAnnouncement.js
+++ b/packages/rocketchat-lib/client/lib/RocketChatAnnouncement.js
@@ -18,4 +18,19 @@ export class RocketChatAnnouncement {
getStyle() {
return this.callback.get();
}
+ getByRoom(rid) {
+ const roomData = Session.get(`roomData${ rid }`);
+ if (!roomData) { return null; }
+ this.room.set(rid);
+ this.message.set(roomData.announcement);
+ this.callback.set(roomData.announcementDetails ? roomData.announcementDetails.callback : null);
+ this.style.set(roomData.announcementDetails ? roomData.announcementDetails.style : null);
+ return this;
+ }
+ clear() {
+ this.message.set(null);
+ this.callback.set(null);
+ this.style.set(null);
+ Meteor.call('saveRoomSettings', this.room.get(), 'roomAnnouncement', {});
+ }
}
diff --git a/packages/rocketchat-lib/client/lib/TabBar.js b/packages/rocketchat-lib/client/lib/TabBar.js
index 714c4c08ee1f..020f180c1186 100644
--- a/packages/rocketchat-lib/client/lib/TabBar.js
+++ b/packages/rocketchat-lib/client/lib/TabBar.js
@@ -1,9 +1,15 @@
import _ from 'underscore';
RocketChat.TabBar = new (class TabBar {
+ get size() {
+ return this._size.get();
+ }
+ set size(s) {
+ this._size.set(s);
+ }
constructor() {
this.buttons = new ReactiveVar({});
-
+ this._size = new ReactiveVar(4);
this.extraGroups = {};
}
diff --git a/packages/rocketchat-livestream/.gitignore b/packages/rocketchat-livestream/.gitignore
new file mode 100644
index 000000000000..f22188fc0aaf
--- /dev/null
+++ b/packages/rocketchat-livestream/.gitignore
@@ -0,0 +1 @@
+.npm/
diff --git a/packages/rocketchat-livestream/client/oauth.js b/packages/rocketchat-livestream/client/oauth.js
new file mode 100644
index 000000000000..73be90578b13
--- /dev/null
+++ b/packages/rocketchat-livestream/client/oauth.js
@@ -0,0 +1,15 @@
+export const close = (popup) => {
+ return new Promise(function(resolve) {
+ const checkInterval = setInterval(() => {
+ if (popup.closed) {
+ clearInterval(checkInterval);
+ resolve();
+ }
+ }, 300);
+ });
+};
+
+export const auth = async() => {
+ const oauthWindow = window.open(`${ RocketChat.settings.get('Site_Url') }/api/v1/livestream/oauth?userId=${ Meteor.userId() }`, 'youtube-integration-oauth', 'width=400,height=600');
+ return await close(oauthWindow);
+};
diff --git a/packages/rocketchat-livestream/client/styles/liveStreamTab.css b/packages/rocketchat-livestream/client/styles/liveStreamTab.css
index 220196796806..862f593b4252 100644
--- a/packages/rocketchat-livestream/client/styles/liveStreamTab.css
+++ b/packages/rocketchat-livestream/client/styles/liveStreamTab.css
@@ -2,27 +2,35 @@
&.livestream {
justify-content: flex-start;
}
+ & .rc-button--stack {
+ width: 100%;
+ }
}
.thumbnail-container {
position: relative;
height: 270px;
- & .--absolute-center {
- position: absolute;
- top: calc(50% - 25px);
- left: calc(50% - 25px);
+ & .popout {
+ &--absolute-center {
+ position: absolute;
+ top: calc(50% - 25px);
+ left: calc(50% - 25px);
- font-size: 50px;
- }
- & .--play-solid {
- fill: var(--rc-color-primary-lightest);;
+ font-size: 50px;
+ }
+ &--play-solid {
+ fill: white;
+ }
}
}
.liveStreamTab__form {
& .rc-switch {
padding: 15px 5px;
}
+ & .rc-input__label p {
+ padding: 10px 0;
+ }
}
.livestream--url {
@@ -37,7 +45,7 @@
& .current-setting {
display: inline-block;
text-decoration: none;
- color: var(--rc-color-primary-dark);
+ color: black;
}
& .rc-button {
diff --git a/packages/rocketchat-livestream/client/tabBar.js b/packages/rocketchat-livestream/client/tabBar.js
index b7409dc3a047..75b5a777f682 100644
--- a/packages/rocketchat-livestream/client/tabBar.js
+++ b/packages/rocketchat-livestream/client/tabBar.js
@@ -1,16 +1,21 @@
Meteor.startup(function() {
Tracker.autorun(function() {
+ RocketChat.TabBar.removeButton('livestream');
if (RocketChat.settings.get('Livestream_enabled')) {
+ const live = RocketChat.models.Rooms.findOne({ _id: Session.get('openedRoom'), 'streamingOptions.id': {$exists :1} }, { fields: { streamingOptions: 1 } });
+ RocketChat.TabBar.size = live ? 5 : 4;
return RocketChat.TabBar.addButton({
groups: ['channel', 'group'],
id: 'livestream',
i18nTitle: 'Livestream',
icon: 'podcast',
template: 'liveStreamTab',
- order: 3
+ order: live ? -1 : 15,
+ class: () => {
+ const roomWithStream = RocketChat.models.Rooms.findOne({ _id: Session.get('openedRoom'), 'streamingOptions.id': {$exists :1} }, { fields: { streamingOptions: 1 } }) || '';
+ return roomWithStream && 'live';
+ }
});
- } else {
- RocketChat.TabBar.removeButton('livestream');
}
});
});
diff --git a/packages/rocketchat-livestream/client/views/broadcastView.html b/packages/rocketchat-livestream/client/views/broadcastView.html
new file mode 100644
index 000000000000..44d69807f7a5
--- /dev/null
+++ b/packages/rocketchat-livestream/client/views/broadcastView.html
@@ -0,0 +1,5 @@
+
+
{{_ "Livestream_not_found" }}
+{{ streamingUnavailableMessage }}
{{/if}} {{#if canEdit}} {{#if editing}} + {{#if broadcastEnabled}} + {{ > liveStreamBroadcast}} + {{/if}} {{else}} diff --git a/packages/rocketchat-livestream/client/views/liveStreamTab.js b/packages/rocketchat-livestream/client/views/liveStreamTab.js index f888906dd144..8d06f777e3c0 100644 --- a/packages/rocketchat-livestream/client/views/liveStreamTab.js +++ b/packages/rocketchat-livestream/client/views/liveStreamTab.js @@ -1,5 +1,29 @@ /* globals popout */ import toastr from 'toastr'; +import { auth } from '../oauth.js'; +import { RocketChatAnnouncement } from 'meteor/rocketchat:lib'; + + +export const call = (...args) => new Promise(function(resolve, reject) { + Meteor.call(...args, function(err, result) { + if (err) { + handleError(err); + reject(err); + } + resolve(result); + }); +}); + +export const close = (popup) => { + return new Promise(function(resolve) { + const checkInterval = setInterval(() => { + if (popup.closed) { + clearInterval(checkInterval); + resolve(); + } + }, 300); + }); +}; function optionsFromUrl(url) { const options = {}; @@ -17,9 +41,15 @@ function optionsFromUrl(url) { } Template.liveStreamTab.helpers({ + broadcastEnabled() { + return !!RocketChat.settings.get('Broadcasting_enabled'); + }, streamingSource() { return Template.instance().streamingOptions.get() ? Template.instance().streamingOptions.get().url : ''; }, + streamingUnavailableMessage() { + return Template.instance().streamingOptions.get() && Template.instance().streamingOptions.get().message && Template.instance().streamingOptions.get().message !== '' ? Template.instance().streamingOptions.get().message : t('Livestream_not_found'); + }, thumbnailUrl() { return Template.instance().streamingOptions.get() ? Template.instance().streamingOptions.get().thumbnail : ''; }, @@ -83,26 +113,55 @@ Template.liveStreamTab.events({ e.preventDefault(); i.editing.set(false); }, + 'click .js-clear'(e, i) { + e.preventDefault(); + + const clearedObject = { + message: i.streamingOptions.get().message || '', + isAudioOnly: i.streamingOptions.get().isAudioOnly || false + }; + + Meteor.call('saveRoomSettings', this.rid, 'streamingOptions', clearedObject, function(err) { + if (err) { + return handleError(err); + } + i.editing.set(false); + i.streamingOptions.set(clearedObject); + const roomAnnouncement = new RocketChatAnnouncement().getByRoom(i.data.rid); + if (roomAnnouncement.getMessage() !== '') { roomAnnouncement.clear(); } + return toastr.success(TAPi18n.__('Livestream_source_changed_succesfully')); + }); + }, 'click .js-save'(e, i) { e.preventDefault(); const streamingOptions = { ...optionsFromUrl(i.find('[name=streaming-source]').value), - isAudioOnly: i.find('[name=streaming-audio-only]').checked + isAudioOnly: i.find('[name=streaming-audio-only]').checked, + message: i.find('[name=streaming-message]').value }; - if (streamingOptions.id != null) { - Meteor.call('saveRoomSettings', this.rid, 'streamingOptions', streamingOptions, function(err) { - if (err) { - return handleError(err); + Meteor.call('saveRoomSettings', this.rid, 'streamingOptions', streamingOptions, function(err) { + if (err) { + return handleError(err); + } + i.editing.set(false); + i.streamingOptions.set(streamingOptions); + if (streamingOptions.url !== '') { + new RocketChatAnnouncement({ + room: i.data.rid, + message: 'Broadcast is now live. Click here to watch!', + callback: 'openBroadcast' + }).save(); + } else { + const roomAnnouncement = new RocketChatAnnouncement().getByRoom(i.data.rid); + if (roomAnnouncement.getMessage() !== '') { + roomAnnouncement.clear(); } - i.editing.set(false); - i.streamingOptions.set(streamingOptions); - return toastr.success(TAPi18n.__('Livestream_source_changed_succesfully')); - }); - } else { - return toastr.error(TAPi18n.__('Livestream_url_incorrect')); - } + } + + return toastr.success(TAPi18n.__('Livestream_source_changed_succesfully')); + }); }, 'click .streaming-source-settings'(e, i) { e.preventDefault(); @@ -138,21 +197,30 @@ Template.liveStreamTab.events({ const streamingOptions = { ...optionsFromUrl(i.find('[name=streaming-source]').value), - isAudioOnly: i.find('[name=streaming-audio-only]').checked + isAudioOnly: i.find('[name=streaming-audio-only]').checked, + message: i.find('[name=streaming-message]').value }; - if (streamingOptions.id != null) { - Meteor.call('saveRoomSettings', this.rid, 'streamingOptions', streamingOptions, function(err) { - if (err) { - return handleError(err); + Meteor.call('saveRoomSettings', this.rid, 'streamingOptions', streamingOptions, function(err) { + if (err) { + return handleError(err); + } + i.editing.set(false); + i.streamingOptions.set(streamingOptions); + if (streamingOptions.url !== '') { + new RocketChatAnnouncement({ + room: i.data.rid, + message: 'Broadcast is now live. Click here to watch!', + callback: 'openBroadcast' + }).save(); + } else { + const roomAnnouncement = new RocketChatAnnouncement().getByRoom(i.data.rid); + if (roomAnnouncement.getMessage() !== '') { + roomAnnouncement.clear(); } - i.editing.set(false); - i.streamingOptions.set(streamingOptions); - return toastr.success(TAPi18n.__('Livestream_source_changed_succesfully')); - }); - } else { - return toastr.error(TAPi18n.__('Livestream_url_incorrect')); - } + } + return toastr.success(TAPi18n.__('Livestream_source_changed_succesfully')); + }); }, 'click .js-popout'(e, i) { e.preventDefault(); @@ -167,5 +235,44 @@ Template.liveStreamTab.events({ onCloseCallback: () => i.popoutOpen.set(false) }); i.popoutOpen.set(true); + }, + async 'click .js-broadcast'(e, i) { + e.preventDefault(); + e.currentTarget.classList.add('loading'); + try { + const user = RocketChat.models.Users.findOne({_id: Meteor.userId()}, { fields: { 'settings.livestream': 1 }}); + if (!user.settings || !user.settings.livestream) { + await auth(); + } + const result = await call('livestreamGet', {rid: i.data.rid}); + popout.open({ + content: 'broadcastView', + data: { + ...result, + showVideoControls: false, + showStreamControls: true + }, + onCloseCallback: () => i.popoutOpen.set(false) + }); + + } catch (e) { + console.log(e); + } finally { + e.currentTarget.classList.remove('loading'); + } } }); + +RocketChat.callbacks.add('openBroadcast', (rid) => { + const roomData = Session.get(`roomData${ rid }`); + if (!roomData) { return; } + popout.open({ + content: 'liveStreamView', + data: { + streamingSource: roomData.streamingOptions.url, + isAudioOnly: roomData.streamingOptions.isAudioOnly, + showVideoControls: true, + streamingOptions: roomData.streamingOptions + } + }); +}); diff --git a/packages/rocketchat-livestream/client/views/livestreamBroadcast.html b/packages/rocketchat-livestream/client/views/livestreamBroadcast.html new file mode 100644 index 000000000000..1340a89ddf95 --- /dev/null +++ b/packages/rocketchat-livestream/client/views/livestreamBroadcast.html @@ -0,0 +1,3 @@ + + + diff --git a/packages/rocketchat-livestream/client/views/livestreamBroadcast.js b/packages/rocketchat-livestream/client/views/livestreamBroadcast.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/rocketchat-livestream/package.js b/packages/rocketchat-livestream/package.js index d145d7e95d55..d9e65bca854e 100644 --- a/packages/rocketchat-livestream/package.js +++ b/packages/rocketchat-livestream/package.js @@ -13,7 +13,11 @@ Package.onUse(function(api) { ]); api.addFiles([ 'client/views/liveStreamTab.html', + 'client/views/livestreamBroadcast.html', + 'client/views/livestreamBroadcast.js', 'client/views/liveStreamTab.js', + 'client/views/broadcastView.html', + 'client/views/broadcastView.js', 'client/styles/liveStreamTab.css', 'client/views/liveStreamView.html', 'client/views/liveStreamView.js', @@ -21,8 +25,13 @@ Package.onUse(function(api) { ], 'client'); api.addFiles([ + 'server/index.js', 'server/models/Rooms.js', 'server/functions/saveStreamingOptions.js', 'server/settings.js' ], 'server'); + + Npm.depends({ + 'googleapis': '25.0.0' + }); }); diff --git a/packages/rocketchat-livestream/server/functions/livestream.js b/packages/rocketchat-livestream/server/functions/livestream.js new file mode 100644 index 000000000000..7affb1e94bf6 --- /dev/null +++ b/packages/rocketchat-livestream/server/functions/livestream.js @@ -0,0 +1,151 @@ +import google from 'googleapis'; +const OAuth2 = google.auth.OAuth2; + + +const p = fn => new Promise(function(resolve, reject) { + fn(function(err, value) { + if (err) { + return reject(err); + } + resolve(value.data); + }); +}); + +export const getBroadcastStatus = async({ + id, + access_token, + refresh_token, + clientId, + clientSecret +}) => { + const auth = new OAuth2(clientId, clientSecret); + + auth.setCredentials({ + access_token, + refresh_token + }); + const youtube = google.youtube({ version:'v3', auth }); + const result = await p(resolve => youtube.liveBroadcasts.list({ + part:'id,status', + id + }, resolve)); + return result.items && result.items[0] && result.items[0].status.lifeCycleStatus; +}; + +export const statusStreamLiveStream = async({ + id, + access_token, + refresh_token, + clientId, + clientSecret +}) => { + const auth = new OAuth2(clientId, clientSecret); + + auth.setCredentials({ + access_token, + refresh_token + }); + + const youtube = google.youtube({ version:'v3', auth }); + const result = await p(resolve => youtube.liveStreams.list({ + part:'id,status', + id + }, resolve)); + return result.items && result.items[0].status.streamStatus; +}; + +export const statusLiveStream = ({ + id, + access_token, + refresh_token, + clientId, + clientSecret, + status +}) => { + const auth = new OAuth2(clientId, clientSecret); + + auth.setCredentials({ + access_token, + refresh_token + }); + + const youtube = google.youtube({ version:'v3', auth }); + + return p(resolve => youtube.liveBroadcasts.transition({ + part:'id,status', + id, + broadcastStatus: status + }, resolve)); +}; + +export const setBroadcastStatus = ({ + id, + access_token, + refresh_token, + clientId, + clientSecret, + status +}) => { + const auth = new OAuth2(clientId, clientSecret); + + auth.setCredentials({ + access_token, + refresh_token + }); + + const youtube = google.youtube({ version:'v3', auth }); + + return p(resolve => youtube.liveBroadcasts.transition({ + part:'id,status', + id, + broadcastStatus: status + }, resolve)); +}; + +export const createLiveStream = async({ + room, + access_token, + refresh_token, + clientId, + clientSecret +}) => { + const auth = new OAuth2(clientId, clientSecret); + auth.setCredentials({ + access_token, + refresh_token + }); + const youtube = google.youtube({ version:'v3', auth }); + + const [stream, broadcast] = await Promise.all([p((resolve) => youtube.liveStreams.insert({ + part: 'id,snippet,cdn,contentDetails,status', + resource: { + snippet: { + 'title': room.name || 'RocketChat Broadcast' + }, + 'cdn': { + 'format': '480p', + 'ingestionType': 'rtmp' + } + } + }, resolve)), p((resolve)=> youtube.liveBroadcasts.insert({ + part: 'id,snippet,contentDetails,status', + resource: { + snippet: { + 'title': room.name || 'RocketChat Broadcast', + 'scheduledStartTime' : new Date().toISOString() + }, + 'status': { + 'privacyStatus': 'unlisted' + } + } + }, resolve))]); + + await p(resolve => youtube.liveBroadcasts.bind({ + part: 'id,snippet,status', + // resource: { + id: broadcast.id, + streamId: stream.id + }, resolve)); + + return {id: stream.cdn.ingestionInfo.streamName, stream, broadcast}; +}; diff --git a/packages/rocketchat-livestream/server/index.js b/packages/rocketchat-livestream/server/index.js new file mode 100644 index 000000000000..63ea14561bf5 --- /dev/null +++ b/packages/rocketchat-livestream/server/index.js @@ -0,0 +1,2 @@ +import './routes.js'; +import './methods.js'; diff --git a/packages/rocketchat-livestream/server/methods.js b/packages/rocketchat-livestream/server/methods.js new file mode 100644 index 000000000000..dbee392045f4 --- /dev/null +++ b/packages/rocketchat-livestream/server/methods.js @@ -0,0 +1,141 @@ +import {Meteor} from 'meteor/meteor'; +import { createLiveStream, statusLiveStream, statusStreamLiveStream, getBroadcastStatus, setBroadcastStatus } from './functions/livestream'; + +const selectLivestreamSettings = (user) => user && user.settings && user.settings.livestream; + +Meteor.methods({ + + async livestreamStreamStatus({streamId}) { + if (!streamId) { + // TODO: change error + throw new Meteor.Error('error-not-allowed', 'Livestream ID not found', { + method: 'livestreamStreamStatus' + }); + } + const livestreamSettings = selectLivestreamSettings(Meteor.user()); + + if (!livestreamSettings) { + throw new Meteor.Error('error-not-allowed', 'You have no settings to stream', { + method: 'livestreamStreamStatus' + }); + } + + const {access_token, refresh_token} = livestreamSettings; + + return await statusStreamLiveStream({ + id: streamId, + access_token, + refresh_token, + clientId: RocketChat.settings.get('Broadcasting_client_id'), + clientSecret: RocketChat.settings.get('Broadcasting_client_secret') + }); + + }, + async setLivestreamStatus({broadcastId, status}) { + if (!broadcastId) { + // TODO: change error + throw new Meteor.Error('error-not-allowed', 'You have no settings to livestream', { + method: 'livestreamStart' + }); + } + const livestreamSettings = selectLivestreamSettings(Meteor.user()); + + if (!livestreamSettings) { + throw new Meteor.Error('error-not-allowed', 'You have no settings to livestream', { + method: 'livestreamStart' + }); + } + + const {access_token, refresh_token} = livestreamSettings; + + return await statusLiveStream({ + id: broadcastId, + access_token, + refresh_token, + status, + clientId: RocketChat.settings.get('Broadcasting_client_id'), + clientSecret: RocketChat.settings.get('Broadcasting_client_secret') + }); + + }, + async livestreamGet({rid}) { + const livestreamSettings = selectLivestreamSettings(Meteor.user()); + + if (!livestreamSettings) { + throw new Meteor.Error('error-not-allowed', 'You have no settings to livestream', { + method: 'livestreamGet' + }); + } + + const room = RocketChat.models.Rooms.findOne({_id: rid}); + + if (!room) { + // TODO: change error + throw new Meteor.Error('error-not-allowed', 'You have no settings to livestream', { + method: 'livestreamGet' + }); + } + + const {access_token, refresh_token} = livestreamSettings; + return await createLiveStream({ + room, + access_token, + refresh_token, + clientId: RocketChat.settings.get('Broadcasting_client_id'), + clientSecret: RocketChat.settings.get('Broadcasting_client_secret') + }); + + }, + async getBroadcastStatus({broadcastId}) { + if (!broadcastId) { + // TODO: change error + throw new Meteor.Error('error-not-allowed', 'Broadcast ID not found', { + method: 'getBroadcastStatus' + }); + } + const livestreamSettings = selectLivestreamSettings(Meteor.user()); + + if (!livestreamSettings) { + throw new Meteor.Error('error-not-allowed', 'You have no settings to stream', { + method: 'getBroadcastStatus' + }); + } + + const {access_token, refresh_token} = livestreamSettings; + + return await getBroadcastStatus({ + id: broadcastId, + access_token, + refresh_token, + clientId: RocketChat.settings.get('Broadcasting_client_id'), + clientSecret: RocketChat.settings.get('Broadcasting_client_secret') + }); + }, + async setBroadcastStatus({broadcastId, status}) { + if (!broadcastId) { + // TODO: change error + throw new Meteor.Error('error-not-allowed', 'Broadcast ID not found', { + method: 'setBroadcastStatus' + }); + } + const livestreamSettings = selectLivestreamSettings(Meteor.user()); + + if (!livestreamSettings) { + throw new Meteor.Error('error-not-allowed', 'You have no settings to stream', { + method: 'setBroadcastStatus' + }); + } + + const {access_token, refresh_token} = livestreamSettings; + + return await setBroadcastStatus({ + id: broadcastId, + access_token, + refresh_token, + status, + clientId: RocketChat.settings.get('Broadcasting_client_id'), + clientSecret: RocketChat.settings.get('Broadcasting_client_secret') + }); + + } +}); diff --git a/packages/rocketchat-livestream/server/routes.js b/packages/rocketchat-livestream/server/routes.js new file mode 100644 index 000000000000..083083c5e4d4 --- /dev/null +++ b/packages/rocketchat-livestream/server/routes.js @@ -0,0 +1,45 @@ +import google from 'googleapis'; +const OAuth2 = google.auth.OAuth2; + +RocketChat.API.v1.addRoute('livestream/oauth', { + get: function functionName() { + const clientAuth = new OAuth2(RocketChat.settings.get('Broadcasting_client_id'), RocketChat.settings.get('Broadcasting_client_secret'), `${ RocketChat.settings.get('Site_Url') }/api/v1/livestream/oauth/callback`.replace(/\/{2}api/g, '/api')); + const { userId } = this.queryParams; + const url = clientAuth.generateAuthUrl({ + access_type: 'offline', + scope: ['https://www.googleapis.com/auth/youtube'], + state: JSON.stringify({ + userId + }) + }); + + return { + statusCode: 302, + headers: { + Location: url + }, body: 'Oauth redirect' + }; + } +}); + +RocketChat.API.v1.addRoute('livestream/oauth/callback', { + get: function functionName() { + const { code, state } = this.queryParams; + + const { userId } = JSON.parse(state); + + const clientAuth = new OAuth2(RocketChat.settings.get('Broadcasting_client_id'), RocketChat.settings.get('Broadcasting_client_secret'), `${ RocketChat.settings.get('Site_Url') }/api/v1/livestream/oauth/callback`.replace(/\/{2}api/g, '/api')); + + const ret = Meteor.wrapAsync(clientAuth.getToken.bind(clientAuth))(code); + + RocketChat.models.Users.update({ _id: userId }, {$set: { + 'settings.livestream' : ret + }}); + + return { + headers: { + 'content-type' : 'text/html' + }, body: '' + }; + } +}); diff --git a/packages/rocketchat-livestream/server/settings.js b/packages/rocketchat-livestream/server/settings.js index 4ef3103d4c68..d195b6a573d6 100644 --- a/packages/rocketchat-livestream/server/settings.js +++ b/packages/rocketchat-livestream/server/settings.js @@ -1,10 +1,22 @@ Meteor.startup(function() { - RocketChat.settings.addGroup('LiveStream', function() { + RocketChat.settings.addGroup('LiveStream & Broadcasting', function() { + this.add('Livestream_enabled', false, { type: 'boolean', - i18nLabel: 'Enabled', public: true, alert: 'This feature is currently in beta! Please report bugs to github.com/RocketChat/Rocket.Chat/issues' }); + + this.add('Broadcasting_enabled', false, { + type: 'boolean', + public: true, + alert: 'This feature is currently in beta! Please report bugs to github.com/RocketChat/Rocket.Chat/issues', + enableQuery: { _id: 'Livestream_enabled', value: true } + }); + + this.add('Broadcasting_client_id', '', { type: 'string', public: false, enableQuery: { _id: 'Broadcasting_enabled', value: true } }); + this.add('Broadcasting_client_secret', '', { type: 'string', public: false, enableQuery: { _id: 'Broadcasting_enabled', value: true } }); + this.add('Broadcasting_api_key', '', { type: 'string', public: false, enableQuery: { _id: 'Broadcasting_enabled', value: true } }); + this.add('Broadcasting_media_server_url', '', { type: 'string', public: true, enableQuery: { _id: 'Broadcasting_enabled', value: true } }); }); }); diff --git a/packages/rocketchat-theme/client/imports/components/header.css b/packages/rocketchat-theme/client/imports/components/header.css index 00be50a788b0..af2f21c43f1a 100644 --- a/packages/rocketchat-theme/client/imports/components/header.css +++ b/packages/rocketchat-theme/client/imports/components/header.css @@ -219,6 +219,25 @@ &:hover { color: var(--rc-color-link-active); } + &.live { + position: relative; + } + &.live:before { + content: ''; + position: absolute; + right: -2px; + bottom: -1px; + display: block; + width: 10px; + width: var(--sidebar-account-status-bullet-size); + height: 10px; + height: var(--sidebar-account-status-bullet-size); + border-radius: 50%; + border-radius: var(--sidebar-account-status-bullet-radius); + background-color: #f5455c; + z-index: 1; + animation: blink 1.5s ease-in-out infinite; + } } &__more { diff --git a/packages/rocketchat-theme/client/imports/components/popout.css b/packages/rocketchat-theme/client/imports/components/popout.css index 3be7f8998851..96ba48dec29d 100644 --- a/packages/rocketchat-theme/client/imports/components/popout.css +++ b/packages/rocketchat-theme/client/imports/components/popout.css @@ -1,3 +1,20 @@ +@keyframes loading { + 0% { + opacity: 0; + transform: scale(0.7); + } + 50% { + opacity: 1; + transform: scale(1); + } + 100% { + opacity: 0; + transform: scale(0.7); + } +} + + + .rc-popout { min-width: 380px; max-width: 800px; @@ -90,6 +107,24 @@ & button { font-size: 16px; margin: 0 5px; + + & > .rc-icon { + fill: currentColor; + stroke: currentColor; + } + + &.preparing { + animation: loading 2s infinite; + pointer-events: none; + } + &.broadcasting { + color: red; + } + } + & span { + text-transform: capitalize; + font-size: 15px; + line-height: 18px; } } diff --git a/packages/rocketchat-ui-flextab/client/flexTabBar.js b/packages/rocketchat-ui-flextab/client/flexTabBar.js index 335c0b1ed87d..7cba2c3cdf87 100644 --- a/packages/rocketchat-ui-flextab/client/flexTabBar.js +++ b/packages/rocketchat-ui-flextab/client/flexTabBar.js @@ -163,7 +163,7 @@ Template.RoomsActionTab.events({ $(e.currentTarget).blur(); e.preventDefault(); const buttons = RocketChat.TabBar.getButtons().filter(button => filterButtons(button, instance.anonymous, instance.data.rid)); - const groups = [{items:(instance.small.get() ? buttons : buttons.slice(4)).map(item => { + const groups = [{items:(instance.small.get() ? buttons : buttons.slice(RocketChat.TabBar.size)).map(item => { item.name = TAPi18n.__(item.i18nTitle); item.action = action; return item; @@ -175,7 +175,7 @@ Template.RoomsActionTab.events({ popoverClass: 'message-box', data: { rid: this._id, - buttons: instance.small.get() ? buttons : buttons.slice(4), + buttons: instance.small.get() ? buttons : buttons.slice(RocketChat.TabBar.size), tabBar: instance.tabBar }, currentTarget: e.currentTarget, @@ -216,7 +216,7 @@ Template.RoomsActionTab.helpers({ return []; } const buttons = RocketChat.TabBar.getButtons().filter(button => filterButtons(button, this.anonymous, this.data.rid)); - return buttons.length <= 5 ? buttons : buttons.slice(0, 4); + return buttons.length <= RocketChat.TabBar.size ? buttons : buttons.slice(0, RocketChat.TabBar.size); }, moreButtons() { @@ -226,6 +226,6 @@ Template.RoomsActionTab.helpers({ const buttons = RocketChat.TabBar.getButtons().filter(button => filterButtons(button, this.anonymous, this.data.rid) ); - return buttons.length > 5; + return buttons.length > RocketChat.TabBar.size; } }); diff --git a/packages/rocketchat-ui/client/views/app/popout.html b/packages/rocketchat-ui/client/views/app/popout.html index 33c2d9287c9d..4d2691f965c4 100644 --- a/packages/rocketchat-ui/client/views/app/popout.html +++ b/packages/rocketchat-ui/client/views/app/popout.html @@ -17,6 +17,12 @@