diff --git a/README.md b/README.md index 2a97a3f..07acafc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,39 @@ -# Project 2 +# Project 2 README.md file -Web Programming with Python and JavaScript + ### Flack Overview: +Flack is a simple messaging application that allows users to register with a display name, create / join channels (chat rooms) and post messages. To post a message, a user simply selects (or creates) a channel and enters text in the message box located at the bottom of the screen. Pressing the "Post" button, adds the message to the active lists and updates anyone else in the channel + +##Personal Touch: +The personal touch includes the addition of messages badges in the user list that tracks and displays the number of messages posted by each user + +## Notes: + +1) The application uses pop-ups to prompt the user for their display name and channel - the browser MUST allow pop-ups for proper functionality of the application. Here is what the prompt looks like: + + + +2) To add channels select the plus (+) icon next to "Channels" - the "General" channel is provided by default just like Slack + +3) New users joining the app are displayed in the left side Users column + + + +4) The application has been tested mostly on Chrome and some on Firefox. + +## Key files and resources: +1) application.py - primary flack application that serves the index.html file, tracks channels / messages and manages user websocket connections. + +2) index.js - client side javascript file which is the core of this application. Handles client interactions with the server via websockets + +3) index.html - the single page application screen base on boostrap + +4) static resources - this includes the index.html file (mentioned above), three graphic files (addGraphic.png, +favicon.ico, and flackUser.png) and the style.css which is mostly used to to properly configure messages + +5) requirements.txt - a list of dependent python modules necessary for running the server + +6) README.md - this file + + + + diff --git a/application.py b/application.py index e8edb33..98b8f83 100644 --- a/application.py +++ b/application.py @@ -1,13 +1,113 @@ import os - -from flask import Flask +import datetime +from flask import Flask, render_template from flask_socketio import SocketIO, emit app = Flask(__name__) + +# Configure to use filesystem session type app.config["SECRET_KEY"] = os.getenv("SECRET_KEY") +app.config["SESSION_PERMANENT"] = False +app.config["SESSION_TYPE"] = "filesystem" + socketio = SocketIO(app) +app.secret_key = os.urandom(32) +# Global lists and dicts +channels = {} +message = {} +messages = [] +users = {} + +MESSAGELIMIT = 100 @app.route("/") +@app.route("/index") def index(): - return "Project 2: TODO" + return render_template("index.html") + +# set up a user who is connecting to the server +@socketio.on('user connect') +def connect(data): + global channels + messages = [] + + # check if channels empty - first time through + if not channels: + # get server date / time + current = datetime.datetime.now() + + displayHeader = " | "+str(current.hour)+":"+str(current.minute)+" | "+str(current.month)+" "+str(current.day)+", "+str(current.year) + + # always provide a general channel just like slack + messages.insert(0, {'username': "System", + 'channel': "General", + 'datetime': displayHeader, + 'message': "Welcome to Flack!"}) + + channels['General'] = messages + emit("select channel", channels, broadcast=False) + else: + # already have some channels so communicate + updatedChannels = [] + + # extract updated list of channels + for key in channels.keys(): + updatedChannels.append(key) + + # emit current channel list + emit("update channels", {'channel': updatedChannels}, broadcast=True) + + # check if user already connected + if (data["username"] not in users.values()): + + # if users{} empty - first time through + if not users: + users.update({0: data["username"]}) + else: + # determine next dict key and increment to add new user + dictKeys = list(users.keys()) + nextKey = max(dictKeys)+1 + users.update({nextKey: data["username"]}) + + # let everyone know a user has joined + print(users) + emit("add user", users, broadcast=True) + + +@socketio.on('get channel') +def getChannel(data): + global channels + updatedChannels = [] + + # emit current channel list + emit("select channel", channels, broadcast=False) + +@socketio.on('new channel') +def addChannel(data): + global channels + updatedChannels = [] + + # add the new channel to the server channel list + channels.update({data['channel']: []}) + + # extract updated list of channels + for key in channels.keys(): + updatedChannels.append(key) + + # let everyone know a new channel is available + emit("update channels", {'channel': updatedChannels}, broadcast=True) + +@socketio.on('post message') +def addMessage(data): + global channels + + # add the message, but keep lists (message queue) to 100 + if len(channels[data['channel']]) > MESSAGELIMIT: + channels.setdefault(data['channel'], []).append(data) + channels.setdefault(data['channel'], []).pop(0) + else: + channels.setdefault(data['channel'], []).append(data) + + # notify everyone of new message + emit("add message", data, broadcast=True) diff --git a/requirements.txt b/requirements.txt index 55ef84d..260d497 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Flask Flask-SocketIO +datetime \ No newline at end of file diff --git a/static/FlackUser.png b/static/FlackUser.png new file mode 100644 index 0000000..1b41190 Binary files /dev/null and b/static/FlackUser.png differ diff --git a/static/Index.js b/static/Index.js new file mode 100644 index 0000000..fd2733a --- /dev/null +++ b/static/Index.js @@ -0,0 +1,269 @@ +// connect to websocket +var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port); + +// globals +var username; +var activeChannel; +var channelList = []; + +var months = ["January", "February", "March", "April", "May", "June", "July", + "August", "September", "October", "November", "December"]; + +// prompt user via popup to enter a new channel +function selectChannel(channelName) { + console.log("selectChannel: " + channelName); + + // make sure messages are going to the right channel + activeChannel = channelName; + localStorage.setItem('channel', activeChannel); + + // source of truth is on the server - request message for the selected channel + socket.emit('get channel', {'channel': channelName}); +} + + +// load prior channel state - part of project requirements +function loadChannel() { + console.log("loadChannel"); + // does user have a prior active channel - must logout cleanly for this to work correctly + if (!localStorage.getItem('channel')) { + localStorage.setItem('channel', "General"); + activeChannel = "General"; + } else { + // otherwise we load previous + activeChannel = localStorage.getItem('channel'); + } + socket.emit('get channel', {'channel': activeChannel}); +} + + +// builds the channel list +function buildChannelList(activeChannel) { + console.log("buildChannel - length = "+channelList.length); + // clear channels + var userList = document.getElementById('channels'); + channels.innerHTML = ''; + + // loop through channel list - set active channel + for (let i = 0; i < channelList.length; i++) { + console.log("constructing channel list..."); + // building a bootstrap formatted channel list + let button = document.createElement('button'); + if (channelList[i] == activeChannel) { + button.setAttribute('class', "list-group-item list-group-item-action active"); + } else { + button.setAttribute('class', "list-group-item list-group-item-action"); + } + button.setAttribute('type', "button"); + button.setAttribute('onclick', "selectChannel('"+channelList[i]+"');"); + button.innerHTML = channelList[i]; + console.log("BUTTON: ", button); + document.querySelector('#channels').append(button); + } + console.log("something went wrong...."); +} + + +// build current message list - user selected another channel +function buildMessageList(messageList) { + console.log("Building message: ", messageList ); + + // clear current message list + var oldMessages = document.getElementById('messages'); + messages.innerHTML = ''; + + //build new message list from server + for (let i = 0; i < messageList.length; i++) { + + // building a bootstrap formatted user message + let msgHeader = "
"; + let msgIdentifier = messageList[i]["username"] + messageList[i]["datetime"]; + let msgSeperator = "

"; + let msgTrailer = "


"; + let msg = msgHeader + msgIdentifier + msgSeperator + messageList[i]["message"] + msgTrailer; + let li = document.createElement('li'); + + // setting message display attributes + li.setAttribute('class', "list-group-item"); + li.setAttribute('id', "posted-message-" +i ); + li.innerHTML = msg; + + document.querySelector('#messages').append(li); + + // adjust view to keep current postings visible + let element = document.getElementById("posted-message-" + i); + element.scrollIntoView(); + } +} + + +// add channel from prompt +function addChannel() { + console.log("addChannel"); + let channel = prompt("Enter a new channel to add...", ""); + if (channel === null) { + return; // user canceled - break out of the function + } + // checking for duplicate channels + if (channelList.indexOf(channel) > -1) { + alert("Duplicate channel name - please try again!"); + + } else { + // not in the array - add the channel + socket.emit('new channel', {'channel': channel}); + } +} + + +// prompt user via popup to enter a username before using Flack +function getUsername() { + console.log("getUsername"); + // can't give an empty response - keep prompting until not empty + while (!username) { + let username = prompt("Enter a display name for Flack: ", ""); + if (!username) { + alert("Username not entered - you must enter a display name to use this site!"); + } else { + return username; + } + } +} + +// check if username stored locally - if not there, we prompt for one +function loadUsername() { + console.log("loadUsername"); + // have we stored a username before + if (!localStorage.getItem('username')) { + username = getUsername(); + localStorage.setItem('username', username ); + localStorage.setItem('message-count', 0 ); + socket.emit('user connect', {'username': username}); + } else { + username = localStorage.getItem('username'); + socket.emit('user connect', {'username': username}); + } +} + +// main document event handler +document.addEventListener('DOMContentLoaded', () => { + + // when connected + socket.on('connect', () => { + + // load the users (display) name from local storage or prompt for one + loadUsername(); + + // load previous channel that user was in or default to general + loadChannel(); + + // user is posting a new message + document.getElementById("messagePost").onclick = () => { + console.log("Post onlick activated..."); + const message = document.querySelector('#message').value; + + // build the datetime for the posted message + let d = new Date(); + let year = d.getFullYear(); + let month = months[d.getMonth()]; + let day = d.getDate(); + let hours = d.getHours(); + let minutes = d.getMinutes(); + + // make human readable 12 hour format + let ampm = hours >= 12 ? 'PM' : 'AM'; + hours = hours % 12; + hours = hours ? hours : 12; // '0' hour is '12' + minutes = minutes < 10 ? '0' + minutes : minutes; + let time = hours + ':' + minutes + ' ' + ampm; + let datetime = " | "+time+" | "+month+" "+day+", "+year; + + socket.emit('post message', {'username': username, 'channel': activeChannel, + 'datetime': datetime, 'message': message} + ); + document.querySelector('#message').value = ''; + }; + }); + + socket.on('select channel', data => { + console.log("SELECT: ", data); + let messageList = data[activeChannel]; + + // extract keys to buld channel list + const keys = Object.keys(data); + for (let i = 0; i < keys.length; i++) { + console.log("update channel list", keys[i]); + channelList[i] = keys[i]; + } + buildMessageList(messageList); + buildChannelList(activeChannel); + + }); + + socket.on('update channels', data => { + console.log("UPDATE: ", data); + + // should be channel list + for (let i = 0; i < data['channel'].length; i++) { + console.log("update channel list", data['channel'][i]); + channelList[i] = data['channel'][i]; + } + buildChannelList(activeChannel); + + }); + + // Post a single message - adding to selected display list + socket.on('add message', data => { + console.log("Adding message...", data); + if (data['channel'] == activeChannel) { + let messageCount = parseInt(localStorage.getItem('message-count')); + + // building a bootstrap formatted user message + let msgHeader = "
"; + let msgIdentifier = data["username"] + data["datetime"]; + let msgSeperator = "

"; + let msgTrailer = "


"; + let msg = msgHeader + msgIdentifier + msgSeperator + data["message"] + msgTrailer; + let li = document.createElement('li'); + + // set attributes + li.setAttribute('class', "list-group-item"); + li.setAttribute('id', "posted-message-" + messageCount); + li.innerHTML = msg; + document.querySelector('#messages').append(li); + document.querySelector("#" + data["username"]).innerHTML = messageCount + 1; + let element = document.getElementById("posted-message-" + messageCount); + element.scrollIntoView(); + localStorage.setItem('message-count', messageCount + 1); + } + }); + + // Add User - when a new user is announced, add to global userList + socket.on('add user', data => { + let messageCount = parseInt(localStorage.getItem('message-count')); + + // remove any old lists + var userList = document.getElementById('users'); + users.innerHTML = ''; + + // update users with current list - duplicates are handled on the server + for (let key in data) { + + // we have a user in the dict to process + if (data.hasOwnProperty(key)) { + console.log("KEY VALUE: ",data[key]); + + // build a bootstrap formatted user list + let li = document.createElement('li'); + li.setAttribute('class',"list-group-item list-group-item-action d-flex justify-content-between align-items-center disabled"); + li.innerHTML = (data[key] + ""+messageCount+""); + document.querySelector('#users').append(li); + } // end if + } // end for + }); // end socket.on add user + +}); + + diff --git a/static/addGraphic.png b/static/addGraphic.png new file mode 100644 index 0000000..c52fa2c Binary files /dev/null and b/static/addGraphic.png differ diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..1b41190 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..8c96f70 --- /dev/null +++ b/static/style.css @@ -0,0 +1,58 @@ +/* Flack css configs */ + +body { + background-color: #f2f2f2; +} + +.btn { + background-color: #f2f2f2; +} + +#messages { +overflow-y: auto; +max-height: calc(95vh - 150px); +} + + +.btn:focus,.btn:active { + outline: none; + box-shadow: none; +} + +.message:focus,.message:active { + outline: none; + box-shadow: none; +} + +.time_date { + color: #747474; + display: block; + font-size: 14px; + margin: 8px 0 0; +} + + .message_text p { + background: #ebebeb none repeat scroll 0 0; + border-radius: 3px; + color: #646464; + font-size: 14px; + margin: 0; + padding: 5px 10px 5px 10px; + width: 100%; +} + +.message_img { + display: inline-block; + width: 6%; +} +.message_content { + display: inline-block; + padding: 5 5 25 10px; + vertical-align: top; + width: 92%; + } + + + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..2f21187 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + Flack + + + +
+ + +
+
+

Flack

+
+
+
+ + +
+
+
+

Users

+
+ + +
+
    + + + +
+
+
+

+
+
+
+

Channels

+ + +
+ + +
+ +
    + + + +
+ +
+
+ + +
+

Messages

+
+
+
+
+
    + + + + +
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+ + \ No newline at end of file