diff --git a/.gitignore b/.gitignore index f43707f..5fdd877 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,11 @@ yarn.lock backend/public/app/bower_components backend/public/app/credentials.js +# StreamBot +stream-bot/config/private +stream-bot/node_modules +stream-bot/.idea + # Frontend iOS/TogetherStream/TogetherStream/Configuration/private.plist diff --git a/README.md b/README.md index af77143..db254f8 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,12 @@ You can skip installing Google Analytics by removing the method `setupGoogleAnal 2. Replace the `sessionSecret` with a unique string. This is used to encrypt the session tokens. 3. Replace the email `userName` and `domainName` with your email address (i.e. `userName@domainName`), the `password` with your email password, and the `displayUserName` and `displayDomainName` with what you want the emails to be sent from. You can use the same values for here as your actual `userName` and `domainName` if you wish. + +### Frontend Configuration +1. In `backend/public/app/credentials.js` replace `YOUR_PRODUCTION_FACEBOOK_APP_ID` and `YOUR_DEVELOPMENT_FACEBOOK_APP_ID` with the ids for your production and development Facebook app ids respectively. +2. Replace `YOUR_YOUTUBE_API_KEY` with Youtube's API key provided by Google. +3. Replace the email `YOUR_CSYNC_SEVER_IP_ADDRESS` and `YOUR_CSYNC_SEVER_PORT` with your csync server's ip address and port. + ### Cloud Configuration 1. Go to https://console.ng.bluemix.net and create an account if you do not already have one. 2. Click "Create a Service", choose "Compose for PostgreSQL" and create it. @@ -94,3 +100,6 @@ Together Stream is intended solely for use with an Apple iOS product and intende ## Licenses [iOS](LICENSE-IOS) [Non-iOS](LICENSE-NON-IOS) + +## Contribution Guide +Want to contribute? Take a look at our [CONTRIBUTING.md](.github/CONTRIBUTING.md) diff --git a/backend/public/app/index.html b/backend/public/app/index.html index 85907ec..1fe5f84 100644 --- a/backend/public/app/index.html +++ b/backend/public/app/index.html @@ -43,28 +43,16 @@ ga('send', 'pageview'); + + - + - diff --git a/backend/public/app/src/behaviors/csync-behavior.html b/backend/public/app/src/behaviors/csync-behavior.html index 135092f..1374281 100644 --- a/backend/public/app/src/behaviors/csync-behavior.html +++ b/backend/public/app/src/behaviors/csync-behavior.html @@ -35,6 +35,38 @@ viewerHeartbeatTimeoutSecs: { type: Number, value: 4000 + }, + /** + * Used to keep track of whether authentication with csync has happened. + */ + csyncAuthSuccess: { + type: Boolean, + value: false + }, + /** + * Used to keep track of when stream is active and when it ends. + */ + isStreamActive: { + type: Boolean, + value: false + }, + /** + * Stores active listeners here for cleanup. + */ + activeListeners: { + type: Array, + value: function () { + return [] + } + }, + /** + * Stores active intervals here for cleanup. + */ + activeIntervals: { + type: Array, + value: function () { + return [] + } } }, /** @@ -63,7 +95,11 @@ let errorCallback = function (err) { console.error(err) } - this.csyncApp.authenticate("facebook", facebookAccessToken).then(successCallback, errorCallback) + let localSuccessCallback = function () { + this.set("csyncAuthSuccess", true) + successCallback() + }.bind(this) + this.csyncApp.authenticate("facebook", facebookAccessToken).then(localSuccessCallback, errorCallback) }, /** * Setting up the csync listeners that change as properties are updated from ios app. @@ -73,6 +109,23 @@ this.setupChatCsyncListener() this.setupHeartbeatCsyncListener() this.setupParticipantsCsyncListener() + this.setupHeartbeatCsyncWriter() + }, + /** + * Called when stream ends to cleanup and allow for another stream to be listened to. + */ + cleanupListeners: function () { + this.set("streamValidated", false) + this.set("chatMessages", []) + + this.activeListeners.forEach(function(listenerKey) { + listenerKey.unlisten() + }) + this.activeIntervals.forEach(function(intervalId) { + clearInterval(intervalId) + }) + this.set("activeListeners", []) + this.set("activeIntervals", []) }, /** * Listens to video key changes. @@ -97,9 +150,16 @@ this.set("currentlyStreamingVideoData.isPlaying", value.data) } else if (value.key.indexOf("streamName") >= 0) { this.set("streamName", value.data) + } else if (value.key.indexOf("isActive") >= 0) { + if (this.isStreamActive && !value.data) { + alert("Stream has ended.") + this.cleanupListeners() + } + this.set("isStreamActive", value.data) } }.bind(this) listenerKey.listen(listenerCallback) + this.push("activeListeners", listenerKey) }, /** * Listens to chat key changes. @@ -121,6 +181,7 @@ }.bind(this) listenerKey.listen(listenerCallback) + this.push("activeListeners", listenerKey) }, /** * Listens to heartbeat key changes to know how many users are currently watching. @@ -138,31 +199,41 @@ if (!value.exists) { return } - this.currentViewerIds[value.creator] = Date.now() - this.set("currentViewerCount", Object.keys(this.currentViewerIds).length) + let currentTimeInSecondsFrom2001 = ((Date.now() - this.timeOffsetMilliseconds) / 1000).toFixed(6) + + // Filtering out invalid heartbeats that are older than ten seconds. + if (currentTimeInSecondsFrom2001 - value.data <= 10) { + this.currentViewerIds[value.creator] = Date.now() + this.set("currentViewerCount", Object.keys(this.currentViewerIds).length) + } }.bind(this) listenerKey.listen(listenerCallback) + this.push("activeListeners", listenerKey) // Setup timer to delete keys that have not received a heartbeat in 2 seconds. - setInterval(function(){ + let intervalId = setInterval(function(){ Object.keys(this.currentViewerIds).forEach(function(key) { if (Date.now() - this.currentViewerIds[key] >= this.viewerHeartbeatTimeoutSecs) { delete this.currentViewerIds[key] } }.bind(this)) this.set("currentViewerCount", Object.keys(this.currentViewerIds).length) - }.bind(this), 250) + }.bind(this), 2000) + this.push("activeIntervals", intervalId) }, /** * Sends heartbeat for logged in user to csync server. */ setupHeartbeatCsyncWriter: function () { - setInterval(function(){ + let intervalId = setInterval(function(){ let writeKey = this.csyncApp.key("streams." + this.streamId + ".heartbeat." + this.loggedInUserFacebookId) - writeKey.write("" + (Date.now() - this.timeOffset) / 1000, {acl: csync.acl.PublicReadWrite}) - }.bind(this), 1000) + let currentTime = ((Date.now() - this.timeOffsetMilliseconds) / 1000).toFixed(6) + + writeKey.write("" + currentTime, {acl: csync.acl.PublicReadWrite}) + }.bind(this), 500) + this.push("activeIntervals", intervalId) }, /** * Listens for when a participant enters/leaves a stream. @@ -184,17 +255,20 @@ }.bind(this) listenerKey.listen(listenerCallback) + this.push("activeListeners", listenerKey) }, /** * Uses Csync to send chat message */ sendChatMessage: function (message) { let writeKey = this.csyncApp.key("streams." + this.streamId + ".chat." + this.generateUUID()) - let timeOffset = this.timeOffset + let timeOffsetMilliseconds = this.timeOffsetMilliseconds + + window.ga('send', 'event', 'button', 'click', 'send_message'); writeKey.write(JSON.stringify({ content: message.detail, id: this.loggedInUserFacebookId, - timestamp: "" + (Date.now() - timeOffset) / 1000 // need to save in seconds from 2001 + timestamp: "" + (Date.now() - timeOffsetMilliseconds) / 1000 // need to save in seconds from 2001 }), {acl: csync.acl.PublicReadWrite}) }, /** @@ -220,17 +294,30 @@ } if (!this.streamValidated && value.key && value.key.indexOf(streamId) >= 0) { - this.set("streamValidated", true) - callback(true) + let isActiveListenerKey = this.csyncApp.key("streams." + streamId + ".isActive") + + isActiveListenerKey.listen(function(error, value) { + // If value doesn't exist and is outdated, don't process it. + if (!value.exists) { + return + } + + if (value.data && !this.streamValidated) { + this.set("streamValidated", true) + callback(true) + isActiveListenerKey.unlisten() + } + }.bind(this)) return } // Checking if invalid once all streams have been checked. - this.debounce("invalidStreamCallback", checkIfInvalid, 200) + this.debounce("invalidStreamCallback", checkIfInvalid, 500) // this.processMessageAndInsertIntoArray(value) }.bind(this) listenerKey.listen(listenerCallback) + this.push("activeListeners", listenerKey) } }; \ No newline at end of file diff --git a/backend/public/app/src/behaviors/facebook-behavior.html b/backend/public/app/src/behaviors/facebook-behavior.html index 956202f..1379c48 100644 --- a/backend/public/app/src/behaviors/facebook-behavior.html +++ b/backend/public/app/src/behaviors/facebook-behavior.html @@ -93,11 +93,10 @@ this.set("isLoggedIn", true) this.set("facebookAccessToken", response.authResponse.accessToken) this.set("loggedInUserFacebookId", response.authResponse.userID) - this.setupHeartbeatCsyncWriter() this.getFBUserInfo(this.loggedInUserFacebookId, function (response) { this.set("loggedInUserFacebookData", response) }.bind(this)) - this.getServerAccessToken(callback) + callback && callback() } else if (forceLogin){ window.FB.login(function(response){ this.handleGetLoginStatus(response, false, callback) diff --git a/backend/public/app/src/shared-styles.html b/backend/public/app/src/shared-styles.html index 8440a91..1830ebd 100644 --- a/backend/public/app/src/shared-styles.html +++ b/backend/public/app/src/shared-styles.html @@ -12,7 +12,7 @@ width: 100%; height: 100%; margin: 0 auto; - font-family: 'Work Sans', 'helvetica Neue', Helvetica, Arial, "Lucida Grande", sans-serif; + font-family: 'Work Sans'; color: rgb(52, 57, 63); font-size: 14pt; overflow-y: scroll; @@ -86,6 +86,7 @@ } .sticky-header { + font-family: "Work Sans"; z-index: 100; position: fixed; top: 0px; @@ -145,7 +146,7 @@ } .sticky-header .logo a { - margin-left: 95px; + margin-left: 20px; } .sticky-header .logo p { @@ -231,6 +232,9 @@ color: white; } + .sticky-header nav ul .logo a { + margin-left: 20px; + } footer .links li { flex: 1 1 auto; } diff --git a/backend/public/app/src/ts-app.html b/backend/public/app/src/ts-app.html index ff8af14..1095a8a 100644 --- a/backend/public/app/src/ts-app.html +++ b/backend/public/app/src/ts-app.html @@ -19,19 +19,71 @@ :host { display: flex; position: relative; - height: 85%; + flex-direction: column; min-height: 300px; padding-top: 60px; + --app-position-footer: fixed; + --app-height: 94%; + height: 94%; + } + .stream-container { + display: flex; + flex-direction:row; + height: 100%; + width: 100%; + } + footer { + position: var(--app-position-footer); + bottom: 0px; + justify-content: center; + height: 5%; + padding: 0px !important; + } + @media (max-width: 963px) { + :host { + flex-direction: column; + padding-top: 0px; + height: var(--app-height) !important; + } + .stream-container { + display: flex; + flex-direction:column-reverse; + height: 100%; + } + footer .links { + margin-top: 20px; + } + footer { + height: auto !important; + display: none; + } } - -