<div id="vue">
  <h1 class="hero">Weaver Messenger V2</h1>
  <router-view></router-view>
</div>
/*

TODO
------
~ Animations & Transitions 
~ fontAwesome

*/

@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400);

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: "Source Sans Pro", sans-serif;
}

$colorArray: #1289FE,
  #075DB2,
  #0B5298,
  #0B5298,
  #0B5298,
  #073765;

$red: #ea0000;
$blue: #1289fe;
$gray: #e5e5ea;

$hero-shadow: 1px 1px 0px $gray, 
  2px 2px 0px $gray, 
  3px 3px 0px $gray;

@keyframes fadeIn {
  0% {
    opacity: 0;
    transform: translateY(50%);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fade {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

@keyframes fadeOut {
  0% {
    opacity: 1;
    transform: translateY(0);
  }
  100% {
    opacity: 0;
    transform: translateY(-50%);
  }
}

.fadeIn {
  animation: fadeIn 0.25s;
  animation-fill-mode: backwards;
  animation-delay: 0.25s;
}

.fadeOut {
  animation: fadeOut 0.25s;
  animation-fill-mode: backwards;
}

#vue {
  margin: 0 auto;
  padding: 1em;
  text-align: center;
  width: 100%;
  max-width: 600px;

  .hero {
    margin: 0.5em auto 0.25em;
    font-size: 3em;
    color: $blue;
    text-shadow: $hero-shadow;
  }

  .login {
    margin: 0;
  }

  .notification {
    margin-top: 0;
    margin-bottom: 1.5em;

    &.bad {
      color: $red;
    }
  }

  input {
    display: block;
    appearance: none;
    margin: 0.5em auto;
    padding: 0.5em;
    vertical-align: middle;
    border: 1px solid $gray;
    border-radius: 2px;
    font-size: 1em;
    width: 100%;
    max-width: 20em;

    &.messageInput {
      display: inline-block;
      width: calc(100% - 3.5em);
    }
  }

  button {
    appearance: none;
    padding: 0.5em;
    vertical-align: middle;
    border: none;
    background: none;
    color: $blue;
    font-weight: 400;
    font-size: 1em;
    cursor: pointer;
    transition: color 0.25s ease-out;

    &:hover {
      color: darken($blue, 15%);
    }
  }

  .messagesWrapper {
    padding: 0.5em;
    max-height: 500px;
    overflow-y: scroll;

    .message {
      margin: 2em auto;
      padding: 0 1em;
      padding-right: 2em;
      background: $gray;
      border-radius: 1em;
      position: relative;
      text-align: left;
      //width: 100%;

      span, p {
        display: inline-block;
        vertical-align: middle;
      }

      span {
        position: absolute;
        top: -1.25em;
        left: 0.75em;
        right: auto;
        font-size: 0.85em;
        color: darken($gray, 15%);
        white-space: nowrap;
        vertical-align: top;

        time {
          display: none;
          vertical-align: top;
        }
      }

      &:hover {
        span {
          time {
            display: inline;
          }
        }
      }

      p {
        width: auto;
      }

      i {
        display: none;
        font-size: 1em;
        vertical-align: middle;
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
        right: 0.5em;
      }

      &[data-person="Me"] {
        background: $blue;
        color: white;

        &:after {
          content: "";
          position: absolute;
          right: -0.5em;
          bottom: 0;
          width: 0.5em;
          height: 1em;
          border-left: 0.5em solid $blue;
          border-bottom-left-radius: 1em 0.5em;
        }

        span {
          right: 0.75em;
          left: auto;
          color: $blue;
        }

        i {
          display: inline-block;
          cursor: pointer;
        }
      }

      &:not([data-person="Me"]) {
        &:after {
          content: "";
          position: absolute;
          left: -0.5em;
          bottom: 0;
          width: 0.5em;
          height: 1em;
          border-right: 0.5em solid $gray;
          border-bottom-right-radius: 1em 0.5em;
        }
      }

      i {
        padding: 0.25em;
        cursor: pointer;
      }
    }
  }

  .usersTyping {
    text-align: left;
    font-size: 0.85em;
    margin: 0 auto;
    color: darken($gray, 15%);
  }

  .usersOnline {
    text-decoration: underline;
    margin-bottom: 0.25em;
  }

  .users {
    margin: 0 auto;
  }

  .addMessage {
    margin: 1em auto;
    text-align: left;
  }

  .chatroomFlexContainer {
    display: flex;
    flex-flow: row wrap;
    justify-content: space-around;
    margin: 1em auto;

    .chatroom {
      position: relative;
      flex-grow: 1;
      margin: 0.5em;
      padding: 1em;
      color: white;
      border-radius: 5px;
      box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);
      transition: all 0.1s;
      cursor: pointer;

      i.fa-close {
        opacity: 0;
        position: absolute;
        top: 0.25em;
        right: 0.25em;
        padding: 0.5em;
        cursor: pointer;
        transition: opacity 0.1s;
      }

      i.fa-cog {
        opacity: 0;
        position: absolute;
        top: 0.25em;
        left: 0.25em;
        padding: 0.5em;
        cursor: pointer;
        transition: opacity 0.1s;
      }


      &:hover {
        box-shadow: 0 12px 15px 0 rgba(0,0,0,0.24), 0 15px 30px 0 rgba(0,0,0,0.19);

        i {
          opacity: 1;
        }
      }

      $i: 1;
      @while $i < length($colorArray)+1 {
        &:nth-of-type(#{$i}n) {
          $color: nth($colorArray, $i);
          background-color: $color;
          $i: $i + 1;
        }
      }
    }
  }

  .modalWrapper {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;

    .overlay {
      position: fixed;
      z-index: 0;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0,0,0,0.5);
    }

    .modal {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background: white;
      z-index: 1;
      padding: 1em;
      border-radius: 5px;

      h2 {
        margin-bottom: 0;
      }

      p.notification {
        margin-top: 0;
        color: $red;
      }

     i.fa-close {
        position: absolute;
        top: 0.5em;
        right: 0.5em;
       cursor: pointer;
      }
    }
  }
}
View Compiled
/*

           TODO
          ------     
          ~ Image uploads? (base64 encode)

          ~ localStorage to remember youre logged in
            ~ use LS from Index

    */


//---------
// Global Data
//---------
var baseURL             = "https://messenger-app-16b49.firebaseio.com",

    chatroomsRef        = new Firebase(baseURL + "/chatrooms"),
    chatroomRef         = chatroomsRef.push(),
    chatroomKey         = "",

    messagesRef         = "",
    messageRef          = "",

    presenceRef         = new Firebase(baseURL + "/.info/connected"),

    usersRef            = new Firebase(baseURL + "/users"),
    userRef             = usersRef.push(),

    myRef               = "",
    myKey               = "",

    newMessage          = {},
    newUser             = {},

    timer               = null,
    loginAttempts       = 0,

    notifications       = {
      "landing":        "Try to be original, your username might be taken.",
      "error":          "We have encountered an error, please reload your page and try again.",
      "invalid":        "Sorry, your password is invalid. Make sure you have everything correct. Caps matter!",
      "loggedIn":       "This user is already logged in.",
      "lockedOut":      "You have attempted to login too many times, please come back later and try again.",
      "duplicateChat":  "Sorry, there is already a chatroom with this name. Try again!"
    },

    loginButtonText = {
      "new":            "Create Account",
      "existing":       "Log In"
    };

//---------
// Components
//---------
var presence = Vue.extend({
  template: '<h3 class="usersOnline">Users Online: {{ users | usersOnline }}</h3>' +
  '<p class="users" v-for="user in users">' +
  '{{ user | isMyUsernameOrOnline }}' +
  '</p>'
  ,

  data: function() {
    return {
      newUser: newUser,
      newMessage: newMessage
    }
  },

  firebase: {
    users: usersRef
  }
});

var login = Vue.extend({
  template: '<div transition="fade">' +
  '<h2 class="login">Login, or create an account below.</h2>' + 
  '<p class="notification {{ notificationStatus }}">{{ notification }}</p>' +
  '<input class="nameInput" @keyup="isExistingUser()" @keyup.enter="goToChatrooms()" v-model="newUser.username" placeholder="Username"/>' +
  '<input class="nameInput" type="password"  @keyup.tab="isExistingUser()" @keyup.enter="goToChatrooms()" v-model="newUser.password" placeholder="Password"/>' +
  '<button @click="goToChatrooms()">{{ loginButtonText }}</button>' +
  '<presence></presence>' +
  '</div>'
  ,

  data: function() {
    return {
      newMessage:         newMessage,
      newUser:            newUser,
      loginAttempts:      loginAttempts,
      notification:       notifications.landing,
      notificationStatus: "good",
      loginButtonText:    loginButtonText.new
    }
  },

  beforeCompile: function() {
    if(myRef != "" && typeof(myRef) != "undefined") {
      myRef.update({typing: false, online: false});
      myRef = "";
      myKey = "";

      //is there a better way?
      newMessage = {
        text:           "",
        username:       "",
        timestamp:      ""
      };
      this.newMessage = newMessage;

      newUser = {
        username:       "",
        password:       ""
      };
      this.newUser =    newUser;
    }
  },

  methods: {
    isExistingUser: function() {    
      for(var i = 0; i < this.users.length; i++) {
        if(this.newUser.username == this.users[i].username) {   
          this.loginButtonText = loginButtonText.existing;
          return;
        }
      }

      this.loginButtonText = loginButtonText.new;     
    },

    goToChatrooms: function() {   
      if(this.loginAttempts < 5) {
        for(var i = 0; i < this.users.length; i++) {
          if(this.newUser.username == this.users[i].username) {    
            if(this.newUser.password != this.users[i].password) {
              this.loginAttempts++;
              this.notificationStatus = "bad";
              this.notification = notifications.invalid;
              return;
            }

            if(!!this.users[i].online) {
              this.loginAttempts++;
              this.notificationStatus = "bad";
              this.notification = notifications["loggedIn"];
              return;
            }

            myKey = this.users[i][".key"];
            myRef = new Firebase(baseURL + "/users/" + myKey);
          }
        }

        if(this.newUser.username.trim() != "" && this.newUser.password.trim() != "") {
          this.newMessage.username = this.newUser.username;
          router.go("/chatrooms");
        }

      } else {
        this.notificationStatus = "bad";
        this.notification = notifications["lockedOut"];
        return;
      }
    }
  },

  firebase: {
    users: usersRef
  }
});

var chatrooms = Vue.extend({
  template: '<div transition="fade">' +
  '<h2 class="createChatroom">Search the current chatrooms, or create a new one.</h2>' + 
  '<input @keyup.esc="resetFilter()" v-model="searchString" type="text" placeholder="Search chatrooms"/>' +
  '<p>- or -</p>' + 
  '<button @click="toggleNewChatroomModal()">Create Chatroom</button>' +
  '<div class="chatroomFlexContainer">' +
  '<div class="chatroom" v-for="chatroom in chatrooms | filterBy searchString" @click="goToMessages(chatroom)">' +
  '<i v-if="chatroom.creator == myUsername" @click.stop="removeChatroom(chatroom[\'.key\'])" class="fa fa-close"></i>' +
  '<i v-if="chatroom.creator == myUsername" @click.stop="toggleEditChatroomModal(chatroom)" class="fa fa-cog"></i>' +
  '<h3>{{ chatroom.name }}</h3>' +
  '<p>{{ chatroom.messages | messageLength }} messages</p>' +
  '</div>' +
  '</div>' +
  '<presence></presence>' + 
  '</div>' +

  '<div v-show="showNewModal" class="modalWrapper" @keyup.esc="toggleNewChatroomModal()">' +
  '<div class="overlay" @click="toggleNewChatroomModal()"></div>' +
  '<div class="modal">' +
  '<i class="fa fa-close" @click="toggleNewChatroomModal()"></i>' +
  '<h2 class="modalHeader">Name your new chatroom</h2>' + 
  '<p>{{ notification }}</p>' + 
  '<input class="modalName" @keyup.enter="createChatroom()" v-model="newChatroomName" placeholder="Chatroom name"/>' +
  '<button @click="createChatroom()">Create Chatroom</button>' +
  '</div>' +
  '</div>' +

  '<div v-show="showEditModal" class="modalWrapper" @keyup.esc="toggleEditChatroomModal()">' +
  '<div class="overlay" @click="toggleEditChatroomModal()"></div>' +
  '<div class="modal">' +
  '<i class="fa fa-close" @click="toggleEditChatroomModal()"></i>' +
  '<h2 class="modalHeader">Edit Chatroom</h2>' + 
  '<p class="notification">{{ notification }}</p>' + 
  '<input class="modalName" @keyup.enter="updateChatroom()" v-model="editChatroomName" placeholder="Chatroom name"/>' +
  '<button @click="updateChatroom()">Update Chatroom Name</button>' +
  '<p>- or -</p>' + 
  '<button @click="resetChatroom()"><i class="fa fa-rotate-left"></i> Reset Chat Messages</button>' + 
  '</div>' +
  '</div>' +
  
  '</div>' 
  ,

  beforeCompile: function() {
    if(newUser.username == "" || newUser.password == "") {
      router.go("/");
      return;
    }    
  },

  ready: function() {    
    var self = this;

    presenceRef.on("value", function(snap) {
      if (snap.val()) {
        if(myKey == "" && newUser.username != "" && newUser.password != "") {
          userRef.set({
            online:   true,
            username: newUser.username,
            password: newUser.password,
            typing:   false

          }, function() {
            myKey = self.users[self.users.length - 1][".key"];
            myRef = new Firebase(baseURL + "/users/" + myKey);
            myRef.onDisconnect().update({typing: false, online: false});
          });

        } else if(typeof(myKey) != "undefined" && myKey != "") {
          myRef.update({online: true});
          myRef.onDisconnect().update({typing: false, online: false});
        }
      }
    });
  },


  methods: {
    toggleNewChatroomModal: function() {
      this.showNewModal = !this.showNewModal;
      this.notification = "";
      this.newChatroomName = "";
    },

    toggleEditChatroomModal: function(chatroom) {
      this.showEditModal = !this.showEditModal;
      this.notification = "";
      
      if(!!chatroom) {
        this.editChatroomName = chatroom.name; 
        this.editChatroomKey = chatroom[".key"];

      } else {
        this.editChatroomName = "";
        this.editChatroomKey = "";
      }
    },

    updateChatroom: function() {
       for(var i = 0; i < this.chatrooms.length; i++) {
        if(this.editChatroomName == this.chatrooms[i].name) {   
          this.notification = notifications.duplicateChat;
          return;
        }
      }
      
      chatroomsRef.child(this.editChatroomKey).update({name: this.editChatroomName});
      this.showEditModal = false;
    },

    resetChatroom: function() {
      chatroomsRef.child(this.editChatroomKey).update({messages: {}});
      this.showEditModal = false;
    },

    removeChatroom: function(key) {
      chatroomsRef.child(key).remove();
    },

    createChatroom: function() {
      var self = this;

      for(var i = 0; i < this.chatrooms.length; i++) {
        if(this.newChatroomName == this.chatrooms[i].name) {   
          this.notification = notifications.duplicateChat;
          return;
        }
      }

      chatroomRef.set({
        name: self.newChatroomName,
        //population: 0,
        messages: {},
        creator: self.myUsername

      }, function() {
        self.toggleNewChatroomModal();
        self.newChatroomName = "";
        chatroomRef = chatroomsRef.push();
      });
    },

    goToMessages: function(chatroom) {
      chatroomKey = chatroom['.key'];
      router.go("/messenger");
    },

    resetFilter: function() {
      this.searchString = "";
    }
  },

  filters: {
    messageLength: function(messages) {
      if(messages) {
        return Object.keys(messages).length;

      } else {
        return 0;
      }
    }
  },

  firebase: {
    chatrooms: chatroomsRef,
    users: usersRef
  },

  data: function() {
    return {
      showNewModal: false,
      showEditModal: false,
      newChatroomName: "",
      editChatroomName: "",
      editChatroomKey: "",
      notification: "",
      searchString: "",
      myUsername: newUser.username
    }
  }
});

var messenger = Vue.extend({
  template: '<div transition="fade">' +
  '<div class="messagesWrapper">' +
  '<div class="message" v-for="message in messages" data-person="{{ message.username | isMyMessage }}">' +
  '<span>{{ message.username | isMyMessage }} <time class="hidden">@ {{ message.timestamp }}</time></span>' +
  '<p>{{ message.text }}</p>' +
  '<i data-person="{{ message.username | isMyMessage }}" @click="removeMessage($key)" class="fa fa-close"></i>' +
  '</div>' +
  '<p class="usersTyping" v-for="user in users">' +
  '{{ user | isTyping }}' +
  '</p>' +
  '</div>' +
  '<input class="messageInput" @keyup.enter="addMessage()" v-model="newMessage.text" @keyup="userTyping($event)"/>' +
  '<button @click="addMessage()">Send</button>' +
  '<presence></presence>' +
  '</div>'
  ,

  beforeCompile: function() {       
    var self = this;

    if(newUser.username == "" || newUser.password == "") {
      router.go("/");
      return;
    }

    messagesRef = new Firebase(baseURL + "/chatrooms/" + chatroomKey + "/messages");
    messageRef = messagesRef.push();    

    messagesRef.once("value", function(snapshot) {
      var data = snapshot.val();
      self.messages = data;
    });
  },

  ready: function() {
    var self = this;

    presenceRef.on("value", function(snap) {
      if (snap.val()) {
        myRef.update({online: true});
        myRef.onDisconnect().update({typing: false, online: false});
      }
    });

    messagesRef.on("value", function(snapshot) {
      var data = snapshot.val();
      self.messages = data;
      self.scrollToBottomOfMessages();
    });    
  },

  data: function() {
    return {
      newMessage: newMessage,
      newUser: newUser,
      messages: {}
    }
  },

  firebase: {
    users: usersRef
  },

  methods: {
    removeMessage: function(key) {
      messagesRef.child(key).remove();
    },

    addMessage: function() {
      if (this.newMessage.text.trim() != "") {
        usersRef.child(myKey).update({typing: false});

        messagesRef.push().set({
          text:       this.newMessage.text,
          username:   this.newMessage.username,
          timestamp:  this.timestamp()
        });

        this.newMessage.text      = "";
        this.newMessage.timestamp = "";
      }
    },

    scrollToBottomOfMessages: function() {
      Vue.nextTick(function() {
        var messages = document.getElementsByClassName("messagesWrapper")[0];
        
        if(!!messages) {
          messages.scrollTop = messages.scrollHeight;
        }
      })
    },

    userTyping: function(e) {
      //only numbers, letters, spaces, and backspace
      if (e.keyCode >= 48 && e.keyCode <= 57 || e.keyCode >= 65 && e.keyCode <= 90 || e.keyCode == 33 || e.keyCode == 4) {
        usersRef.child(myKey).update({typing: true});

        clearTimeout(timer); 
        timer = setTimeout(function() {
          usersRef.child(myKey).update({typing: false});
        }, 1000);
      }
    },

    timestamp: function() {
      var date = new Date();

      function formatDate(date) {
        var monthNames = [
          "January", "February", "March",
          "April", "May", "June", "July",
          "August", "September", "October",
          "November", "December"
        ];

        var day         = date.getDate(),
            monthIndex  = date.getMonth(),
            year        = date.getFullYear();

        return monthNames[monthIndex] + " " + day + ", " +  year
      }

      function formatTime(date) {
        var hour    = date.getHours(),
            minute  = date.getMinutes(),
            amPM    = (hour > 11) ? "pm" : "am";

        if(hour > 12) {
          hour -= 12;
        } else if(hour == 0) {
          hour = "12";
        }
        if(minute < 10) {
          minute = "0" + minute;
        }

        return hour + ":" + minute + amPM;
      }


      return formatTime(date) + " " + formatDate(date)
    },

    convertImgToBase64: function(url, callback, outputFormat){
      var img = new Image();
      img.crossOrigin = 'Anonymous';
      img.onload = function() {
        var canvas = document.createElement('CANVAS');
        var ctx = canvas.getContext('2d');
        canvas.height = this.height;
        canvas.width = this.width;
        ctx.drawImage(this,0,0);
        var dataURL = canvas.toDataURL(outputFormat || 'image/png');
        callback(dataURL);
        canvas = null; 
      };
      img.src = url;
    },


    encodeLocalImageFileAsURL: function(cb) {
      return function(){
        var file = this.files[0];
        var reader = new FileReader();
        reader.onloadend = function() {
          cb(reader.result);
        }
        reader.readAsDataURL(file);
      }
    }
  },

  filters: {
    isMyMessage: function(username) {
      if(username === this.newMessage.username) {
        return "Me"
      } else {
        return username
      }
    },

    isTyping: function(user) {
      var self = this;

      if(user.typing && user.username != self.newMessage.username) {
        self.scrollToBottomOfMessages();
        return user.username + " is typing..."
      }
    }
  }
});

//---------
// Global Filters
//---------
Vue.filter("isMyUsernameOrOnline", function(user) {
  if(user.username === this.newMessage.username) {
    return user.username + " (Me)"
  } else {
    if(user.online == true) {
      return user.username
    }
  }
});

Vue.filter("usersOnline", function(users) {  
  var numberOnline = 0;

  for(var i = 0; i < users.length; i++) {
    if(users[i].online == true) {
      numberOnline++;
    }
  }

  return numberOnline;
});

//---------
// Transitions
//---------
Vue.transition('fade', {
  enterClass: 'fadeIn',
  leaveClass: 'fadeOut'
})

//---------
// Register global components
//---------
Vue.component('presence', presence)

//---------
// Router
//---------
var router = new VueRouter();
router.map({
  '/': {
    component: login
  },

  '/chatrooms': {
    component: chatrooms
  },

  '/messenger': {
    component: messenger
  }
})
router.start(Vue, '#vue');
Run Pen

External CSS

  1. https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.21/vue.js
  2. https://cdnjs.cloudflare.com/ajax/libs/vue-router/0.7.11/vue-router.js
  3. https://cdn.firebase.com/js/client/2.4.2/firebase.js
  4. https://cdn.jsdelivr.net/vuefire/1.3.0/vuefire.min.js