diff --git a/.meteor/packages b/.meteor/packages
index 3caa7be9e165499ccb1e792cba05949087fd1dc3..0585cdb7b02f6ecbfce67bec4bbf03db819f2a12 100644
--- a/.meteor/packages
+++ b/.meteor/packages
@@ -93,6 +93,7 @@ rocketchat:slashcommands-me
 rocketchat:slashcommands-mute
 rocketchat:slashcommands-topic
 rocketchat:slashcommands-unarchive
+rocketchat:smarsh-connector
 rocketchat:spotify
 rocketchat:statistics
 rocketchat:streamer
diff --git a/.meteor/versions b/.meteor/versions
index 9d194ace4cb3b203b730f73b472ee65c83b815f5..f0319f51d354a18feeaac19325db3ba65226ac80 100644
--- a/.meteor/versions
+++ b/.meteor/versions
@@ -189,6 +189,7 @@ rocketchat:slashcommands-mute@0.0.1
 rocketchat:slashcommands-open@0.0.1
 rocketchat:slashcommands-topic@0.0.1
 rocketchat:slashcommands-unarchive@0.0.1
+rocketchat:smarsh-connector@0.0.1
 rocketchat:sms@0.0.1
 rocketchat:spotify@0.0.1
 rocketchat:statistics@0.0.1
diff --git a/.sandstorm/sandstorm-pkgdef.capnp b/.sandstorm/sandstorm-pkgdef.capnp
index e05904382916053459dd623e3804907aca92bb1c..95b054c07280b96b2a121aa3b09fb53e45de0162 100644
--- a/.sandstorm/sandstorm-pkgdef.capnp
+++ b/.sandstorm/sandstorm-pkgdef.capnp
@@ -21,7 +21,7 @@ const pkgdef :Spk.PackageDefinition = (
 
 		appVersion = 38,  # Increment this for every release.
 
-		appMarketingVersion = (defaultText = "0.37.1"),
+		appMarketingVersion = (defaultText = "0.38.0"),
 		# Human-readable representation of appVersion. Should match the way you
 		# identify versions of your app in documentation and marketing.
 
diff --git a/HISTORY.md b/HISTORY.md
index d9b10f8ed44803d616989ed72fd319eb2cd98f68..bd1e025731f007fb3829fe561c37bde09e45d42a 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,5 +1,40 @@
 ## NEXT
 
+## 0.38.0, 2016-Aug-30
+
+- Action links improvements
+- Add global event unread-changed-by-subscription
+- Add role to disable/enable channel preview (#4127)
+- Add room setting to require code to join Room (#4126)
+- Add the timer for disconnecting, one minute after going in the background it'll disconnect
+- Add Ubuntu 16.04-under 30 seconds snap deployment using SNAPS
+- Added File Uploaded text on attachments to i18n
+- Added option to populate Rocket Chat with LDAP users (import them) (#4054)
+- Changes rtl check in ChatMessages class (#4049)
+- Check message timestamp before notifying users
+- Do not check for last admin while updating a user
+- Don't send offline emails to users who aren't active
+- Fix mispelling for seriliazedDescriptor
+- Fix multiple notifications (closes #3517) (#4074)
+- Fix offering Sandstorm grains without a title
+- Fix the verbs in Sandstorm activity events
+- Fix user update check for last admin
+- Fixed buttons margins and upload file list
+- Formatting and adding some missing permissions to standard roles
+- Handle locations when disabled
+- Improve lazy loading of custom fields and translations
+- Improve stream broadcast connection (#4119)
+- Improvements/login and registration (#4073)
+- Less borders (#4101)
+- Make sure Sandstorm.notify is always called for DMs
+- Open room correctly after creation and new messages
+- Set gitlabs scope to 'api', the only support scope.
+- Set message.ts if empty on sendMessage method
+- Update moment locales
+- Update to Autolinker.js 0.28.0
+- Update to depend only on the gMaps API key, add i18n strings for geolocaiotn sharing
+- Updated loginform a11y and UX - labels instead of placeholders (#4075)
+
 ## 0.37.1, 2016-Aug-17
 
 - Allow deletion of records with same id on settings
diff --git a/README.md b/README.md
index 7b4fb699d3092567a9d2c082565751140ac42c6f..fb04b826603faa2709baa38415f277bcc0f503c3 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@
    * [Sandstorm.io](#sandstormio) 
    * [DPlatform](#dplatform) 
    * [IndieHosters](#indiehosters)
+   * [Ubuntu 16.04](#ubuntu-1604)
    * [Cloudron.io](#cloudronio) 
    * [Nitrous.io](#nitrousio)
    * [Heroku](#heroku)
@@ -28,7 +29,6 @@
   * [Raspberry Pi 2](#raspberry-pi-2)
   * [Koozali SME](#koozali-sme)
   * [Ubuntu VPS](#ubuntu-vps)
-  * [Ubuntu Software Center](#ubuntu-software-center)
 * [About Rocket.Chat](#about-rocketchat)
   * [On the News](#on-the-news)
   * [Features](#features)
@@ -90,6 +90,15 @@ Get your Rocket.Chat instance hosted in a "as a Service" style. You register and
 
 [![Rocket.Chat on IndieHosters](https://indie.host/signup.png)](https://indiehosters.net/shop/product/rocket-chat-21)
 
+## Ubuntu 16.04
+[![Ubuntu Apps Explorer](https://raw.githubusercontent.com/Sing-Li/bbug/master/images/uappexplorer.png)](https://uappexplorer.com/app/rocketchat-server.rocketchat)
+
+Deploy from shell:
+
+`snap install rocketchat-server`
+
+In under 30 seconds, your Rocket.Chat server will be up and running at `http://host-ip:3000`
+
 ## Cloudron.io
 
 Install Rocket.Chat on [Cloudron](https://cloudron.io) Smartserver:
@@ -172,11 +181,6 @@ Add Rocket.Chat to this world famous time tested small enterprise server today:
 ## Ubuntu VPS
 Follow these [deployment instructions](https://rocket.chat/docs/installation/manual-installation/ubuntu/)
 
-## Ubuntu Software Center
-Easy one click install right from your Ubuntu Desktop (coming soon)
-
-[![Ubuntu Software Center](https://raw.githubusercontent.com/Sing-Li/bbug/master/images/ubuntusoft.png)]()
-
 # About Rocket.Chat
 
 Rocket.Chat is a Web Chat Server, developed in JavaScript, using the [Meteor](https://www.meteor.com/install) fullstack framework.
@@ -246,7 +250,7 @@ It is a great solution for communities and companies wanting to privately host t
 - Audio calls
 - Multi-users Audio Conference
 - Screensharing
-- XMPP bridge ([try it](https://demo.rocket.chat/channel/xmppbridge))
+- XMPP bridge ([try it](https://demo.rocket.chat/channel/general))
 - REST APIs
 - Remote Locations Video Monitoring
 - Native real-time APIs for Microsoft C#, Visual Basic, F# and other .NET supported languages ([Get it!](https://www.nuget.org/packages/Rocket.Chat.Net/0.0.12-pre))
diff --git a/client/startup/emailVerification.js b/client/startup/emailVerification.js
index cae42e4b19b3dc3c636ad935261fc41b7139ce5e..5be8d18838030a603ed5a5cc60510f57ce02c4e2 100644
--- a/client/startup/emailVerification.js
+++ b/client/startup/emailVerification.js
@@ -1,6 +1,7 @@
 Meteor.startup(function() {
 	Tracker.autorun(function() {
-		if (Meteor.user() && Meteor.user().emails && Meteor.user().emails[0] && Meteor.user().emails[0].verified !== true && RocketChat.settings.get('Accounts_EmailVerification') === true && !Session.get('Accounts_EmailVerification_Warning')) {
+		var user = Meteor.user();
+		if (user && user.emails && user.emails[0] && user.emails[0].verified !== true && RocketChat.settings.get('Accounts_EmailVerification') === true && !Session.get('Accounts_EmailVerification_Warning')) {
 			toastr.warning(TAPi18n.__('You_have_not_verified_your_email'));
 			Session.set('Accounts_EmailVerification_Warning', true);
 		}
diff --git a/client/startup/userSetUtcOffset.js b/client/startup/userSetUtcOffset.js
new file mode 100644
index 0000000000000000000000000000000000000000..27444599d9135f46b5bcbfedee986d7bdbe016db
--- /dev/null
+++ b/client/startup/userSetUtcOffset.js
@@ -0,0 +1,12 @@
+Meteor.startup(function() {
+	Tracker.autorun(function() {
+		var user, utcOffset;
+		user = Meteor.user();
+		if (user && user.statusConnection === 'online') {
+			utcOffset = moment().utcOffset() / 60;
+			if (user.utcOffset !== utcOffset) {
+				Meteor.call('userSetUtcOffset', utcOffset);
+			}
+		}
+	});
+});
diff --git a/client/startup/userTimezone.coffee b/client/startup/userTimezone.coffee
deleted file mode 100644
index 0d35691abb6f8904ec455d21de5d769d11baf2df..0000000000000000000000000000000000000000
--- a/client/startup/userTimezone.coffee
+++ /dev/null
@@ -1,6 +0,0 @@
-Tracker.autorun ->
-	user = Meteor.user()
-	if user?.statusConnection is 'online'
-		utcOffset = moment().utcOffset() / 60
-		if user.utcOffset isnt utcOffset
-			Meteor.call 'updateUserUtcOffset', utcOffset
\ No newline at end of file
diff --git a/packages/meteor-accounts-saml/saml_server.js b/packages/meteor-accounts-saml/saml_server.js
index 4c6816a16af3c83bbfe06a398e06196fef9e5d13..e5d6359200d1f9abb6b77cb628449b595cc2c610 100644
--- a/packages/meteor-accounts-saml/saml_server.js
+++ b/packages/meteor-accounts-saml/saml_server.js
@@ -110,6 +110,8 @@ Accounts.registerLoginHandler(function(loginRequest) {
 				if (username) {
 					newUser.username = username;
 				}
+			} else if (loginResult.profile.username) {
+				newUser.username = loginResult.profile.username;
 			}
 
 			var userId = Accounts.insertUserDoc({}, newUser);
diff --git a/packages/meteor-autocomplete/autocomplete-client.coffee b/packages/meteor-autocomplete/autocomplete-client.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..b58875785139042817b8edf03b41f032651c88c2
--- /dev/null
+++ b/packages/meteor-autocomplete/autocomplete-client.coffee
@@ -0,0 +1,367 @@
+AutoCompleteRecords = new Mongo.Collection("autocompleteRecords")
+
+isServerSearch = (rule) -> _.isString(rule.collection)
+
+validateRule = (rule) ->
+  if rule.subscription? and not Match.test(rule.collection, String)
+    throw new Error("Collection name must be specified as string for server-side search")
+
+  # XXX back-compat message, to be removed
+  if rule.callback?
+    console.warn("autocomplete no longer supports callbacks; use event listeners instead.")
+
+isWholeField = (rule) ->
+  # either '' or null both count as whole field.
+  return !rule.token
+
+getRegExp = (rule) ->
+  unless isWholeField(rule)
+    # Expressions for the range from the last word break to the current cursor position
+    new RegExp('(^|\\b|\\s)' + rule.token + '([\\w.]*)$')
+  else
+    # Whole-field behavior - word characters or spaces
+    new RegExp('(^)(.*)$')
+
+getFindParams = (rule, filter, limit) ->
+  # This is a different 'filter' - the selector from the settings
+  # We need to extend so that we don't copy over rule.filter
+  selector = _.extend({}, rule.filter || {})
+  options = { limit: limit }
+
+  # Match anything, no sort, limit X
+  return [ selector, options ] unless filter
+
+  if rule.sort and rule.field
+    sortspec = {}
+    # Only sort if there is a filter, for faster performance on a match of anything
+    sortspec[rule.field] = 1
+    options.sort = sortspec
+
+  if _.isFunction(rule.selector)
+    # Custom selector
+    _.extend(selector, rule.selector(filter))
+  else
+    selector[rule.field] = {
+      $regex: if rule.matchAll then filter else "^" + filter
+      # default is case insensitive search - empty string is not the same as undefined!
+      $options: if (typeof rule.options is 'undefined') then 'i' else rule.options
+    }
+
+  return [ selector, options ]
+
+getField = (obj, str) ->
+  obj = obj[key] for key in str.split(".")
+  return obj
+
+class @AutoComplete
+
+  @KEYS: [
+    40, # DOWN
+    38, # UP
+    13, # ENTER
+    27, # ESCAPE
+    9   # TAB
+  ]
+
+  constructor: (settings) ->
+    @limit = settings.limit || 5
+    @position = settings.position || "bottom"
+
+    @rules = settings.rules
+    validateRule(rule) for rule in @rules
+
+    @expressions = (getRegExp(rule) for rule in @rules)
+
+    @matched = -1
+    @loaded = true
+
+    # Reactive dependencies for current matching rule and filter
+    @ruleDep = new Deps.Dependency
+    @filterDep = new Deps.Dependency
+    @loadingDep = new Deps.Dependency
+
+    # autosubscribe to the record set published by the server based on the filter
+    # This will tear down server subscriptions when they are no longer being used.
+    @sub = null
+    @comp = Deps.autorun =>
+      # Stop any existing sub immediately, don't wait
+      @sub?.stop()
+
+      return unless (rule = @matchedRule()) and (filter = @getFilter()) isnt null
+
+      # subscribe only for server-side collections
+      unless isServerSearch(rule)
+        @setLoaded(true) # Immediately loaded
+        return
+
+      [ selector, options ] = getFindParams(rule, filter, @limit)
+
+      # console.debug 'Subscribing to <%s> in <%s>.<%s>', filter, rule.collection, rule.field
+      @setLoaded(false)
+      subName = rule.subscription || "autocomplete-recordset"
+      @sub = Meteor.subscribe(subName,
+        selector, options, rule.collection, => @setLoaded(true))
+
+  teardown: ->
+    # Stop the reactive computation we started for this autocomplete instance
+    @comp.stop()
+
+  # reactive getters and setters for @filter and the currently matched rule
+  matchedRule: ->
+    @ruleDep.depend()
+    if @matched >= 0 then @rules[@matched] else null
+
+  setMatchedRule: (i) ->
+    @matched = i
+    @ruleDep.changed()
+
+  getFilter: ->
+    @filterDep.depend()
+    return @filter
+
+  setFilter: (x) ->
+    @filter = x
+    @filterDep.changed()
+    return @filter
+
+  isLoaded: ->
+    @loadingDep.depend()
+    return @loaded
+
+  setLoaded: (val) ->
+    return if val is @loaded # Don't cause redraws unnecessarily
+    @loaded = val
+    @loadingDep.changed()
+
+  onKeyUp: ->
+    return unless @$element # Don't try to do this while loading
+    startpos = @element.selectionStart
+    val = @getText().substring(0, startpos)
+
+    ###
+      Matching on multiple expressions.
+      We always go from a matched state to an unmatched one
+      before going to a different matched one.
+    ###
+    i = 0
+    breakLoop = false
+    while i < @expressions.length
+      matches = val.match(@expressions[i])
+
+      # matching -> not matching
+      if not matches and @matched is i
+        @setMatchedRule(-1)
+        breakLoop = true
+
+      # not matching -> matching
+      if matches and @matched is -1
+        @setMatchedRule(i)
+        breakLoop = true
+
+      # Did filter change?
+      if matches and @filter isnt matches[2]
+        @setFilter(matches[2])
+        breakLoop = true
+
+      break if breakLoop
+      i++
+
+  onKeyDown: (e) ->
+    return if @matched is -1 or (@constructor.KEYS.indexOf(e.keyCode) < 0)
+
+    switch e.keyCode
+      when 9, 13 # TAB, ENTER
+        if @select() # Don't jump fields or submit if select successful
+          e.preventDefault()
+          e.stopPropagation()
+      # preventDefault needed below to avoid moving cursor when selecting
+      when 40 # DOWN
+        e.preventDefault()
+        @next()
+      when 38 # UP
+        e.preventDefault()
+        @prev()
+      when 27 # ESCAPE
+        @$element.blur()
+        @hideList()
+
+    return
+
+  onFocus: ->
+    # We need to run onKeyUp after the focus resolves,
+    # or the caret position (selectionStart) will not be correct
+    Meteor.defer => @onKeyUp()
+
+  onBlur: ->
+    # We need to delay this so click events work
+    # TODO this is a bit of a hack; see if we can't be smarter
+    Meteor.setTimeout =>
+      @hideList()
+    , 500
+
+  onItemClick: (doc, e) => @processSelection(doc, @rules[@matched])
+
+  onItemHover: (doc, e) ->
+    @tmplInst.$(".-autocomplete-item").removeClass("selected")
+    $(e.target).closest(".-autocomplete-item").addClass("selected")
+
+  filteredList: ->
+    # @ruleDep.depend() # optional as long as we use depend on filter, because list will always get re-rendered
+    filter = @getFilter() # Reactively depend on the filter
+    return null if @matched is -1
+
+    rule = @rules[@matched]
+    # Don't display list unless we have a token or a filter (or both)
+    # Single field: nothing displayed until something is typed
+    return null unless rule.token or filter
+
+    [ selector, options ] = getFindParams(rule, filter, @limit)
+
+    Meteor.defer => @ensureSelection()
+
+    # if server collection, the server has already done the filtering work
+    return AutoCompleteRecords.find({}, options) if isServerSearch(rule)
+
+    # Otherwise, search on client
+    return rule.collection.find(selector, options)
+
+  isShowing: ->
+    rule = @matchedRule()
+    # Same rules as above
+    showing = rule? and (rule.token or @getFilter())
+
+    # Do this after the render
+    if showing
+      Meteor.defer =>
+        @positionContainer()
+        @ensureSelection()
+
+    return showing
+
+  # Replace text with currently selected item
+  select: ->
+    node = @tmplInst.find(".-autocomplete-item.selected")
+    return false unless node?
+    doc = Blaze.getData(node)
+    return false unless doc # Don't select if nothing matched
+
+    @processSelection(doc, @rules[@matched])
+    return true
+
+  processSelection: (doc, rule) ->
+    replacement = getField(doc, rule.field)
+
+    unless isWholeField(rule)
+      @replace(replacement, rule)
+      @hideList()
+
+    else
+      # Empty string or doesn't exist?
+      # Single-field replacement: replace whole field
+      @setText(replacement)
+
+      # Field retains focus, but list is hidden unless another key is pressed
+      # Must be deferred or onKeyUp will trigger and match again
+      # TODO this is a hack; see above
+      @onBlur()
+
+    @$element.trigger("autocompleteselect", doc)
+    return
+
+  # Replace the appropriate region
+  replace: (replacement) ->
+    startpos = @element.selectionStart
+    fullStuff = @getText()
+    val = fullStuff.substring(0, startpos)
+    val = val.replace(@expressions[@matched], "$1" + @rules[@matched].token + replacement)
+    posfix = fullStuff.substring(startpos, fullStuff.length)
+    separator = (if posfix.match(/^\s/) then "" else " ")
+    finalFight = val + separator + posfix
+    @setText finalFight
+
+    newPosition = val.length + 1
+    @element.setSelectionRange(newPosition, newPosition)
+    return
+
+  hideList: ->
+    @setMatchedRule(-1)
+    @setFilter(null)
+
+  getText: ->
+    return @$element.val() || @$element.text()
+
+  setText: (text) ->
+    if @$element.is("input,textarea")
+      @$element.val(text)
+    else
+      @$element.html(text)
+
+  ###
+    Rendering functions
+  ###
+  positionContainer: ->
+    # First render; Pick the first item and set css whenever list gets shown
+    position = @$element.position()
+
+    rule = @matchedRule()
+
+    offset = getCaretCoordinates(@element, @element.selectionStart)
+
+    # In whole-field positioning, we don't move the container and make it the
+    # full width of the field.
+    if rule? and isWholeField(rule)
+      pos =
+        left: position.left
+        width: @$element.outerWidth()               # position.offsetWidth
+    else # Normal positioning, at token word
+      pos =
+        left: position.left + offset.left
+
+    # Position menu from top (above) or from bottom of caret (below, default)
+    if @position is "top"
+      pos.bottom = @$element.offsetParent().height() - position.top - offset.top
+    else
+      pos.top = position.top + offset.top + parseInt(@$element.css('font-size'))
+
+    @tmplInst.$(".-autocomplete-container").css(pos)
+
+  ensureSelection : ->
+    # Re-render; make sure selected item is something in the list or none if list empty
+    selectedItem = @tmplInst.$(".-autocomplete-item.selected")
+
+    unless selectedItem.length
+      # Select anything
+      @tmplInst.$(".-autocomplete-item:first-child").addClass("selected")
+
+  # Select next item in list
+  next: ->
+    currentItem = @tmplInst.$(".-autocomplete-item.selected")
+    return unless currentItem.length # Don't try to iterate an empty list
+    currentItem.removeClass("selected")
+
+    next = currentItem.next()
+    if next.length
+      next.addClass("selected")
+    else # End of list or lost selection; Go back to first item
+      @tmplInst.$(".-autocomplete-item:first-child").addClass("selected")
+
+  # Select previous item in list
+  prev: ->
+    currentItem = @tmplInst.$(".-autocomplete-item.selected")
+    return unless currentItem.length # Don't try to iterate an empty list
+    currentItem.removeClass("selected")
+
+    prev = currentItem.prev()
+    if prev.length
+      prev.addClass("selected")
+    else # Beginning of list or lost selection; Go to end of list
+      @tmplInst.$(".-autocomplete-item:last-child").addClass("selected")
+
+  # This doesn't need to be reactive because list already changes reactively
+  # and will cause all of the items to re-render anyway
+  currentTemplate: -> @rules[@matched].template
+
+AutocompleteTest =
+  records: AutoCompleteRecords
+  getRegExp: getRegExp
+  getFindParams: getFindParams
diff --git a/packages/meteor-autocomplete/autocomplete-server.coffee b/packages/meteor-autocomplete/autocomplete-server.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..192d5479bb20a72935a8730bb583391fee7100a6
--- /dev/null
+++ b/packages/meteor-autocomplete/autocomplete-server.coffee
@@ -0,0 +1,27 @@
+class Autocomplete
+  @publishCursor: (cursor, sub) ->
+    # This also attaches an onStop callback to sub, so we don't need to worry about that.
+    # https://github.com/meteor/meteor/blob/devel/packages/mongo/collection.js
+    Mongo.Collection._publishCursor(cursor, sub, "autocompleteRecords")
+
+Meteor.publish 'autocomplete-recordset', (selector, options, collName) ->
+  collection = global[collName]
+  unless collection
+    throw new Error(collName + ' is not defined on the global namespace of the server.')
+
+  # This is a semi-documented Meteor feature:
+  # https://github.com/meteor/meteor/blob/devel/packages/mongo-livedata/collection.js
+  unless collection._isInsecure()
+    Meteor._debug(collName + ' is a secure collection, therefore no data was returned because the client could compromise security by subscribing to arbitrary server collections via the browser console. Please write your own publish function.')
+    return [] # We need this for the subscription to be marked ready
+
+  # guard against client-side DOS: hard limit to 50
+  options.limit = Math.min(50, Math.abs(options.limit)) if options.limit
+
+  # Push this into our own collection on the client so they don't interfere with other publications of the named collection.
+  # This also stops the observer automatically when the subscription is stopped.
+  Autocomplete.publishCursor( collection.find(selector, options), this)
+
+  # Mark the subscription ready after the initial addition of documents.
+  this.ready()
+
diff --git a/packages/meteor-autocomplete/autocomplete.css b/packages/meteor-autocomplete/autocomplete.css
new file mode 100755
index 0000000000000000000000000000000000000000..c4af66c2ff879b18f509445c8859b1fadd3eedea
--- /dev/null
+++ b/packages/meteor-autocomplete/autocomplete.css
@@ -0,0 +1,27 @@
+.-autocomplete-container {
+    position: absolute;
+    background: white;
+    border: 1px solid #DDD;
+    border-radius: 3px;
+    box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
+    min-width: 180px;
+    z-index: 1000;
+}
+
+.-autocomplete-list {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+}
+
+.-autocomplete-item {
+    display: block;
+    padding: 5px 10px;
+    border-bottom: 1px solid #DDD;
+}
+
+.-autocomplete-item.selected {
+    color: white;
+    background: #4183C4;
+    text-decoration: none;
+}
diff --git a/packages/meteor-autocomplete/inputs.html b/packages/meteor-autocomplete/inputs.html
new file mode 100755
index 0000000000000000000000000000000000000000..289caa58ff7e8e50c7288dab4ae926f5e78f2aca
--- /dev/null
+++ b/packages/meteor-autocomplete/inputs.html
@@ -0,0 +1,39 @@
+<template name="inputAutocomplete">
+    <input type="text" {{attributes}}>
+    {{> autocompleteContainer}}
+</template>
+
+<template name="textareaAutocomplete">
+    <textarea {{attributes}}>{{> UI.contentBlock}}</textarea>
+    {{> autocompleteContainer}}
+</template>
+
+<template name="_autocompleteContainer">
+    {{#if isShowing}}
+    <div class='-autocomplete-container'>
+        {{#if isLoaded}}
+            {{#unless empty}}
+            <ul class='-autocomplete-list'>
+                {{#each filteredList}}
+                <li class="-autocomplete-item">
+                    {{#with ../currentTemplate }}
+                        {{#with ..}}  {{! original 'data' context to itemTemplate}}
+                            {{> ..}}  {{! return value from itemTemplate }}
+                        {{/with}}
+                    {{/with}}
+                </li>
+                {{/each}}
+            </ul>
+            {{else}}
+                {{> noMatchTemplate }}
+            {{/unless}}
+        {{else}}
+            {{> loading}}
+        {{/if}}
+    </div>
+    {{/if}}
+</template>
+
+<template name="_noMatch">
+    (<i>no matches</i>)
+</template>
diff --git a/packages/meteor-autocomplete/package.js b/packages/meteor-autocomplete/package.js
new file mode 100755
index 0000000000000000000000000000000000000000..8e459c8df749fbbb3c634cc2f9398d8d621ed53e
--- /dev/null
+++ b/packages/meteor-autocomplete/package.js
@@ -0,0 +1,44 @@
+Package.describe({
+	name: 'mizzao:autocomplete',
+	summary: 'Client/server autocompletion designed for Meteor\'s collections and reactivity',
+	version: '0.5.1',
+	git: 'https://github.com/mizzao/meteor-autocomplete.git'
+});
+
+Package.onUse(function(api) {
+	api.versionsFrom('1.0');
+
+	api.use(['blaze', 'templating', 'jquery'], 'client');
+	api.use(['coffeescript', 'underscore']); // both
+	api.use(['mongo', 'ddp']);
+
+	api.use('dandv:caret-position@2.1.0-3', 'client');
+
+	// Our files
+	api.addFiles([
+		'autocomplete.css',
+		'inputs.html',
+		'autocomplete-client.coffee',
+		'templates.coffee'
+	], 'client');
+
+	api.addFiles([
+		'autocomplete-server.coffee'
+	], 'server');
+
+	api.export('Autocomplete', 'server');
+	api.export('AutocompleteTest', {testOnly: true});
+});
+
+Package.onTest(function(api) {
+	api.use('mizzao:autocomplete');
+
+	api.use('coffeescript');
+	api.use('mongo');
+	api.use('tinytest');
+
+	api.addFiles('tests/rule_tests.coffee', 'client');
+	api.addFiles('tests/regex_tests.coffee', 'client');
+	api.addFiles('tests/param_tests.coffee', 'client');
+	api.addFiles('tests/security_tests.coffee');
+});
diff --git a/packages/meteor-autocomplete/templates.coffee b/packages/meteor-autocomplete/templates.coffee
new file mode 100755
index 0000000000000000000000000000000000000000..cd56d8ba739846aa43fde4f8deca69d1eaf5dd63
--- /dev/null
+++ b/packages/meteor-autocomplete/templates.coffee
@@ -0,0 +1,50 @@
+# Events on template instances, sent to the autocomplete class
+acEvents =
+  "keydown": (e, t) -> t.ac.onKeyDown(e)
+  "keyup": (e, t) -> t.ac.onKeyUp(e)
+  "focus": (e, t) -> t.ac.onFocus(e)
+  "blur": (e, t) -> t.ac.onBlur(e)
+
+Template.inputAutocomplete.events(acEvents)
+Template.textareaAutocomplete.events(acEvents)
+
+attributes = -> _.omit(@, 'settings') # Render all but the settings parameter
+
+autocompleteHelpers = {
+  attributes,
+  autocompleteContainer: new Template('AutocompleteContainer', ->
+    ac = new AutoComplete( Blaze.getData().settings )
+    # Set the autocomplete object on the parent template instance
+    this.parentView.templateInstance().ac = ac
+
+    # Set nodes on render in the autocomplete class
+    this.onViewReady ->
+      ac.element = this.parentView.firstNode()
+      ac.$element = $(ac.element)
+
+    return Blaze.With(ac, -> Template._autocompleteContainer)
+  )
+}
+
+Template.inputAutocomplete.helpers(autocompleteHelpers)
+Template.textareaAutocomplete.helpers(autocompleteHelpers)
+
+Template._autocompleteContainer.rendered = ->
+  @data.tmplInst = this
+
+Template._autocompleteContainer.destroyed = ->
+  # Meteor._debug "autocomplete destroyed"
+  @data.teardown()
+
+###
+  List rendering helpers
+###
+
+Template._autocompleteContainer.events
+  # t.data is the AutoComplete instance; `this` is the data item
+  "click .-autocomplete-item": (e, t) -> t.data.onItemClick(this, e)
+  "mouseenter .-autocomplete-item": (e, t) -> t.data.onItemHover(this, e)
+
+Template._autocompleteContainer.helpers
+  empty: -> @filteredList().count() is 0
+  noMatchTemplate: -> @matchedRule().noMatchTemplate || Template._noMatch
diff --git a/packages/rocketchat-authorization/client/lib/models/Subscriptions.js b/packages/rocketchat-authorization/client/lib/models/Subscriptions.js
index ac5aab09a6dbcd11dc8d11a9583e37d6c42104d2..3d948a2722337ceea9904d93544548b2555bc787 100644
--- a/packages/rocketchat-authorization/client/lib/models/Subscriptions.js
+++ b/packages/rocketchat-authorization/client/lib/models/Subscriptions.js
@@ -3,6 +3,10 @@ if (_.isUndefined(RocketChat.models.Subscriptions)) {
 }
 
 RocketChat.models.Subscriptions.isUserInRole = function(userId, roleName, roomId) {
+	if (roomId == null) {
+		return false;
+	}
+
 	var query = {
 		rid: roomId,
 		roles: roleName
diff --git a/packages/rocketchat-authorization/server/functions/addUserRoles.coffee b/packages/rocketchat-authorization/server/functions/addUserRoles.coffee
index c715153ce326174249ddba7290f2051551558997..647c25d68269ad8695e18755c716f0dbd883d241 100644
--- a/packages/rocketchat-authorization/server/functions/addUserRoles.coffee
+++ b/packages/rocketchat-authorization/server/functions/addUserRoles.coffee
@@ -4,7 +4,7 @@ RocketChat.authz.addUserRoles = (userId, roleNames, scope) ->
 
 	user = RocketChat.models.Users.findOneById(userId)
 	if not user
-		throw new Meteor.Error 'invalid-user'
+		throw new Meteor.Error 'error-invalid-user', 'Invalid user', { function: 'RocketChat.authz.addUserRoles' }
 
 	roleNames = [].concat roleNames
 
diff --git a/packages/rocketchat-authorization/server/models/Base.js b/packages/rocketchat-authorization/server/models/Base.js
index a2463bc07055b84bafa5fd7b28921667081f14c0..cc9d4dd67ce0225233d479ff52eca2df1581e153 100644
--- a/packages/rocketchat-authorization/server/models/Base.js
+++ b/packages/rocketchat-authorization/server/models/Base.js
@@ -1,5 +1,5 @@
 RocketChat.models._Base.prototype.roleBaseQuery = function(/*userId, scope*/) {
-	return {};
+	return;
 };
 
 RocketChat.models._Base.prototype.findRolesByUserId = function(userId/*, options*/) {
@@ -9,6 +9,11 @@ RocketChat.models._Base.prototype.findRolesByUserId = function(userId/*, options
 
 RocketChat.models._Base.prototype.isUserInRole = function(userId, roleName, scope) {
 	var query = this.roleBaseQuery(userId, scope);
+
+	if (query == null) {
+		return false;
+	}
+
 	query.roles = roleName;
 	return !_.isUndefined(this.findOne(query));
 };
diff --git a/packages/rocketchat-authorization/server/models/Subscriptions.js b/packages/rocketchat-authorization/server/models/Subscriptions.js
index e55fdd75373eb16ec2a6597855813c64ec751ffa..06a0f62e2221cf80c8e4ea57e694ac843713f72a 100644
--- a/packages/rocketchat-authorization/server/models/Subscriptions.js
+++ b/packages/rocketchat-authorization/server/models/Subscriptions.js
@@ -1,4 +1,8 @@
 RocketChat.models.Subscriptions.roleBaseQuery = function(userId, scope) {
+	if (scope == null) {
+		return;
+	}
+
 	var query = { 'u._id': userId };
 	if (!_.isUndefined(scope)) {
 		query.rid = scope;
diff --git a/packages/rocketchat-authorization/server/startup.coffee b/packages/rocketchat-authorization/server/startup.coffee
index 907d0c3ea616096e5dd284ef7a5901248c173c14..d4feba4af6632d5f397c847f70c5743c4f8c1fa1 100644
--- a/packages/rocketchat-authorization/server/startup.coffee
+++ b/packages/rocketchat-authorization/server/startup.coffee
@@ -46,6 +46,7 @@ Meteor.startup ->
 		{ _id: 'view-full-other-user-info',     roles : ['admin'] }
 		{ _id: 'view-history',                  roles : ['admin', 'user'] }
 		{ _id: 'view-joined-room',              roles : ['guest', 'bot'] }
+		{ _id: 'view-join-code',                roles : ['admin'] }
 		{ _id: 'view-logs',                     roles : ['admin'] }
 		{ _id: 'view-other-user-channels',      roles : ['admin'] }
 		{ _id: 'view-p-room',                   roles : ['admin', 'user'] }
@@ -53,6 +54,7 @@ Meteor.startup ->
 		{ _id: 'view-room-administration',      roles : ['admin'] }
 		{ _id: 'view-statistics',               roles : ['admin'] }
 		{ _id: 'view-user-administration',      roles : ['admin'] }
+		{ _id: 'preview-c-room',                roles : ['admin', 'user'] }
 	]
 
 	for permission in permissions
diff --git a/packages/rocketchat-autolinker/lib/Autolinker.min.js b/packages/rocketchat-autolinker/lib/Autolinker.min.js
index 45023691fb1d32d2242c21d9eb73000965be9f35..c518e9185a251d1664e8231e76c4e620bc80105f 100644
--- a/packages/rocketchat-autolinker/lib/Autolinker.min.js
+++ b/packages/rocketchat-autolinker/lib/Autolinker.min.js
@@ -1,10 +1,10 @@
 /*!
  * Autolinker.js
- * 0.26.0
+ * 0.28.0
  *
  * Copyright(c) 2016 Gregory Jacobs <greg@greg-jacobs.com>
  * MIT License
  *
  * https://github.com/gregjacobs/Autolinker.js
  */
-!function(t,e){"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?module.exports=e():t.Autolinker=e()}(this,function(){var t=function(e){e=e||{},this.version=t.version,this.urls=this.normalizeUrlsCfg(e.urls),this.email="boolean"==typeof e.email?e.email:!0,this.twitter="boolean"==typeof e.twitter?e.twitter:!0,this.phone="boolean"==typeof e.phone?e.phone:!0,this.hashtag=e.hashtag||!1,this.newWindow="boolean"==typeof e.newWindow?e.newWindow:!0,this.stripPrefix="boolean"==typeof e.stripPrefix?e.stripPrefix:!0;var r=this.hashtag;if(r!==!1&&"twitter"!==r&&"facebook"!==r&&"instagram"!==r)throw new Error("invalid `hashtag` cfg - see docs");this.truncate=this.normalizeTruncateCfg(e.truncate),this.className=e.className||"",this.replaceFn=e.replaceFn||null,this.htmlParser=null,this.matchers=null,this.tagBuilder=null};return t.link=function(e,r){var a=new t(r);return a.link(e)},t.version="0.26.0",t.prototype={constructor:t,normalizeUrlsCfg:function(t){return null==t&&(t=!0),"boolean"==typeof t?{schemeMatches:t,wwwMatches:t,tldMatches:t}:{schemeMatches:"boolean"==typeof t.schemeMatches?t.schemeMatches:!0,wwwMatches:"boolean"==typeof t.wwwMatches?t.wwwMatches:!0,tldMatches:"boolean"==typeof t.tldMatches?t.tldMatches:!0}},normalizeTruncateCfg:function(e){return"number"==typeof e?{length:e,location:"end"}:t.Util.defaults(e||{},{length:Number.POSITIVE_INFINITY,location:"end"})},parse:function(t){for(var e=this.getHtmlParser(),r=e.parse(t),a=0,n=[],i=0,s=r.length;s>i;i++){var o=r[i],c=o.getType();if("element"===c&&"a"===o.getTagName())o.isClosing()?a=Math.max(a-1,0):a++;else if("text"===c&&0===a){var h=this.parseText(o.getText(),o.getOffset());n.push.apply(n,h)}}return n=this.compactMatches(n),n=this.removeUnwantedMatches(n)},compactMatches:function(t){t.sort(function(t,e){return t.getOffset()-e.getOffset()});for(var e=0;e<t.length-1;e++)for(var r=t[e],a=r.getOffset()+r.getMatchedText().length;e+1<t.length&&t[e+1].getOffset()<=a;)t.splice(e+1,1);return t},removeUnwantedMatches:function(e){var r=t.Util.remove;return this.hashtag||r(e,function(t){return"hashtag"===t.getType()}),this.email||r(e,function(t){return"email"===t.getType()}),this.phone||r(e,function(t){return"phone"===t.getType()}),this.twitter||r(e,function(t){return"twitter"===t.getType()}),this.urls.schemeMatches||r(e,function(t){return"url"===t.getType()&&"scheme"===t.getUrlMatchType()}),this.urls.wwwMatches||r(e,function(t){return"url"===t.getType()&&"www"===t.getUrlMatchType()}),this.urls.tldMatches||r(e,function(t){return"url"===t.getType()&&"tld"===t.getUrlMatchType()}),e},parseText:function(t,e){e=e||0;for(var r=this.getMatchers(),a=[],n=0,i=r.length;i>n;n++){for(var s=r[n].parseMatches(t),o=0,c=s.length;c>o;o++)s[o].setOffset(e+s[o].getOffset());a.push.apply(a,s)}return a},link:function(t){if(!t)return"";for(var e=this.parse(t),r=[],a=0,n=0,i=e.length;i>n;n++){var s=e[n];r.push(t.substring(a,s.getOffset())),r.push(this.createMatchReturnVal(s)),a=s.getOffset()+s.getMatchedText().length}return r.push(t.substring(a)),r.join("")},createMatchReturnVal:function(e){var r;if(this.replaceFn&&(r=this.replaceFn.call(this,this,e)),"string"==typeof r)return r;if(r===!1)return e.getMatchedText();if(r instanceof t.HtmlTag)return r.toAnchorString();var a=e.buildTag();return a.toAnchorString()},getHtmlParser:function(){var e=this.htmlParser;return e||(e=this.htmlParser=new t.htmlParser.HtmlParser),e},getMatchers:function(){if(this.matchers)return this.matchers;var e=t.matcher,r=this.getTagBuilder(),a=[new e.Hashtag({tagBuilder:r,serviceName:this.hashtag}),new e.Email({tagBuilder:r}),new e.Phone({tagBuilder:r}),new e.Twitter({tagBuilder:r}),new e.Url({tagBuilder:r,stripPrefix:this.stripPrefix})];return this.matchers=a},getTagBuilder:function(){var e=this.tagBuilder;return e||(e=this.tagBuilder=new t.AnchorTagBuilder({newWindow:this.newWindow,truncate:this.truncate,className:this.className})),e}},t.match={},t.matcher={},t.htmlParser={},t.truncate={},t.Util={abstractMethod:function(){throw"abstract"},trimRegex:/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,assign:function(t,e){for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);return t},defaults:function(t,e){for(var r in e)e.hasOwnProperty(r)&&void 0===t[r]&&(t[r]=e[r]);return t},extend:function(e,r){var a=e.prototype,n=function(){};n.prototype=a;var i;i=r.hasOwnProperty("constructor")?r.constructor:function(){a.constructor.apply(this,arguments)};var s=i.prototype=new n;return s.constructor=i,s.superclass=a,delete r.constructor,t.Util.assign(s,r),i},ellipsis:function(t,e,r){return t.length>e&&(r=null==r?"..":r,t=t.substring(0,e-r.length)+r),t},indexOf:function(t,e){if(Array.prototype.indexOf)return t.indexOf(e);for(var r=0,a=t.length;a>r;r++)if(t[r]===e)return r;return-1},remove:function(t,e){for(var r=t.length-1;r>=0;r--)e(t[r])===!0&&t.splice(r,1)},splitAndCapture:function(t,e){for(var r,a=[],n=0;r=e.exec(t);)a.push(t.substring(n,r.index)),a.push(r[0]),n=r.index+r[0].length;return a.push(t.substring(n)),a},trim:function(t){return t.replace(this.trimRegex,"")}},t.HtmlTag=t.Util.extend(Object,{whitespaceRegex:/\s+/,constructor:function(e){t.Util.assign(this,e),this.innerHtml=this.innerHtml||this.innerHTML},setTagName:function(t){return this.tagName=t,this},getTagName:function(){return this.tagName||""},setAttr:function(t,e){var r=this.getAttrs();return r[t]=e,this},getAttr:function(t){return this.getAttrs()[t]},setAttrs:function(e){var r=this.getAttrs();return t.Util.assign(r,e),this},getAttrs:function(){return this.attrs||(this.attrs={})},setClass:function(t){return this.setAttr("class",t)},addClass:function(e){for(var r,a=this.getClass(),n=this.whitespaceRegex,i=t.Util.indexOf,s=a?a.split(n):[],o=e.split(n);r=o.shift();)-1===i(s,r)&&s.push(r);return this.getAttrs()["class"]=s.join(" "),this},removeClass:function(e){for(var r,a=this.getClass(),n=this.whitespaceRegex,i=t.Util.indexOf,s=a?a.split(n):[],o=e.split(n);s.length&&(r=o.shift());){var c=i(s,r);-1!==c&&s.splice(c,1)}return this.getAttrs()["class"]=s.join(" "),this},getClass:function(){return this.getAttrs()["class"]||""},hasClass:function(t){return-1!==(" "+this.getClass()+" ").indexOf(" "+t+" ")},setInnerHtml:function(t){return this.innerHtml=t,this},getInnerHtml:function(){return this.innerHtml||""},toAnchorString:function(){var t=this.getTagName(),e=this.buildAttrsStr();return e=e?" "+e:"",["<",t,e,">",this.getInnerHtml(),"</",t,">"].join("")},buildAttrsStr:function(){if(!this.attrs)return"";var t=this.getAttrs(),e=[];for(var r in t)t.hasOwnProperty(r)&&e.push(r+'="'+t[r]+'"');return e.join(" ")}}),t.RegexLib=function(){var t="A-Za-z\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-ˁˆ-ˑˠ-ˤˬˮͰ-ʹͶͷͺ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԯԱ-Ֆՙա-ևא-תװ-ײؠ-يٮٯٱ-ۓەۥۦۮۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪߴߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࢠ-ࢴऄ-हऽॐक़-ॡॱ-ঀঅ-ঌএঐও-নপ-রলশ-হঽৎড়ঢ়য়-ৡৰৱਅ-ਊਏਐਓ-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલળવ-હઽૐૠૡૹଅ-ଌଏଐଓ-ନପ-ରଲଳଵ-ହଽଡ଼ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கஙசஜஞடணதந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-హఽౘ-ౚౠౡಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-ഐഒ-ഺഽൎൟ-ൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะาำเ-ๆກຂຄງຈຊຍດ-ທນ-ຟມ-ຣລວສຫອ-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿၐ-ၕၚ-ၝၡၥၦၮ-ၰၵ-ႁႎႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏽᏸ-ᏽᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᛱ-ᛸᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៗៜᠠ-ᡷᢀ-ᢨᢪᢰ-ᣵᤀ-ᤞᥐ-ᥭᥰ-ᥴᦀ-ᦫᦰ-ᧉᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮᮯᮺ-ᯥᰀ-ᰣᱍ-ᱏᱚ-ᱽᳩ-ᳬᳮ-ᳱᳵᳶᴀ-ᶿḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼⁱⁿₐ-ₜℂℇℊ-ℓℕℙ-ℝℤΩℨK-ℭℯ-ℹℼ-ℿⅅ-ⅉⅎↃↄⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞⸯ々〆〱-〵〻〼ぁ-ゖゝ-ゟァ-ヺー-ヿㄅ-ㄭㄱ-ㆎㆠ-ㆺㇰ-ㇿ㐀-䶵一-鿕ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘟꘪꘫꙀ-ꙮꙿ-ꚝꚠ-ꛥꜗ-ꜟꜢ-ꞈꞋ-ꞭꞰ-ꞷꟷ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꣽꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲꧏꧠ-ꧤꧦ-ꧯꧺ-ꧾꨀ-ꨨꩀ-ꩂꩄ-ꩋꩠ-ꩶꩺꩾ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ꫝꫠ-ꫪꫲ-ꫴꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꬰ-ꭚꭜ-ꭥꭰ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִײַ-ﬨשׁ-זּטּ-לּמּנּסּףּפּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA-Za-zヲ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ",e="0-9٠-٩۰-۹߀-߉०-९০-৯੦-੯૦-૯୦-୯௦-௯౦-౯೦-೯൦-൯෦-෯๐-๙໐-໙༠-༩၀-၉႐-႙០-៩᠐-᠙᥆-᥏᧐-᧙᪀-᪉᪐-᪙᭐-᭙᮰-᮹᱀-᱉᱐-᱙꘠-꘩꣐-꣙꤀-꤉꧐-꧙꧰-꧹꩐-꩙꯰-꯹0-9",r=t+e,a=new RegExp("["+r+".\\-]*["+r+"\\-]"),n=/(?:travelersinsurance|sandvikcoromant|kerryproperties|cancerresearch|weatherchannel|kerrylogistics|spreadbetting|international|wolterskluwer|lifeinsurance|construction|pamperedchef|scholarships|versicherung|bridgestone|creditunion|kerryhotels|investments|productions|blackfriday|enterprises|lamborghini|photography|motorcycles|williamhill|playstation|contractors|barclaycard|accountants|redumbrella|engineering|management|telefonica|protection|consulting|tatamotors|creditcard|vlaanderen|schaeffler|associates|properties|foundation|republican|bnpparibas|boehringer|eurovision|extraspace|industries|immobilien|university|technology|volkswagen|healthcare|restaurant|cuisinella|vistaprint|apartments|accountant|travelers|homedepot|institute|vacations|furniture|fresenius|insurance|christmas|bloomberg|solutions|barcelona|firestone|financial|kuokgroup|fairwinds|community|passagens|goldpoint|equipment|lifestyle|yodobashi|aquarelle|marketing|analytics|education|amsterdam|statefarm|melbourne|allfinanz|directory|microsoft|stockholm|montblanc|accenture|lancaster|landrover|everbank|istanbul|graphics|grainger|ipiranga|softbank|attorney|pharmacy|saarland|catering|airforce|yokohama|mortgage|frontier|mutuelle|stcgroup|memorial|pictures|football|symantec|cipriani|ventures|telecity|cityeats|verisign|flsmidth|boutique|cleaning|firmdale|clinique|clothing|redstone|infiniti|deloitte|feedback|services|broadway|plumbing|commbank|training|barclays|exchange|computer|brussels|software|delivery|barefoot|builders|business|bargains|engineer|holdings|download|security|helsinki|lighting|movistar|discount|hdfcbank|supplies|marriott|property|diamonds|capetown|partners|democrat|jpmorgan|bradesco|budapest|rexroth|zuerich|shriram|academy|science|support|youtube|singles|surgery|alibaba|statoil|dentist|schwarz|android|cruises|cricket|digital|markets|starhub|systems|courses|coupons|netbank|country|domains|corsica|network|neustar|realtor|lincoln|limited|schmidt|yamaxun|cooking|contact|auction|spiegel|liaison|leclerc|latrobe|lasalle|abogado|compare|lanxess|exposed|express|company|cologne|college|avianca|lacaixa|fashion|recipes|ferrero|komatsu|storage|wanggou|clubmed|sandvik|fishing|fitness|bauhaus|kitchen|flights|florist|flowers|watches|weather|temasek|samsung|bentley|forsale|channel|theater|frogans|theatre|okinawa|website|tickets|jewelry|gallery|tiffany|iselect|shiksha|brother|organic|wedding|genting|toshiba|origins|philips|hyundai|hotmail|hoteles|hosting|rentals|windows|cartier|bugatti|holiday|careers|whoswho|hitachi|panerai|caravan|reviews|guitars|capital|trading|hamburg|hangout|finance|stream|family|abbott|health|review|travel|report|hermes|hiphop|gratis|career|toyota|hockey|dating|repair|google|social|soccer|reisen|global|otsuka|giving|unicom|casino|photos|center|broker|rocher|orange|bostik|garden|insure|ryukyu|bharti|safety|physio|sakura|oracle|online|jaguar|gallup|piaget|tienda|futbol|pictet|joburg|webcam|berlin|office|juegos|kaufen|chanel|chrome|xihuan|church|tennis|circle|kinder|flickr|bayern|claims|clinic|viajes|nowruz|xperia|norton|yachts|studio|coffee|camera|sanofi|nissan|author|expert|events|comsec|lawyer|tattoo|viking|estate|villas|condos|realty|yandex|energy|emerck|virgin|vision|durban|living|school|coupon|london|taobao|natura|taipei|nagoya|luxury|walter|aramco|sydney|madrid|credit|maison|makeup|schule|market|anquan|direct|design|swatch|suzuki|alsace|vuelos|dental|alipay|voyage|shouji|voting|airtel|mutual|degree|supply|agency|museum|mobily|dealer|monash|select|mormon|active|moscow|racing|datsun|quebec|nissay|rodeo|email|gifts|works|photo|chloe|edeka|cheap|earth|vista|tushu|koeln|glass|shoes|globo|tunes|gmail|nokia|space|kyoto|black|ricoh|seven|lamer|sener|epson|cisco|praxi|trust|citic|crown|shell|lease|green|legal|lexus|ninja|tatar|gripe|nikon|group|video|wales|autos|gucci|party|nexus|guide|linde|adult|parts|amica|lixil|boats|azure|loans|locus|cymru|lotte|lotto|stada|click|poker|quest|dabur|lupin|nadex|paris|faith|dance|canon|place|gives|trade|skype|rocks|mango|cloud|boots|smile|final|swiss|homes|honda|media|horse|cards|deals|watch|bosch|house|pizza|miami|osaka|tours|total|xerox|coach|sucks|style|delta|toray|iinet|tools|money|codes|beats|tokyo|salon|archi|movie|baidu|study|actor|yahoo|store|apple|world|forex|today|bible|tmall|tirol|irish|tires|forum|reise|vegas|vodka|sharp|omega|weber|jetzt|audio|promo|build|bingo|chase|gallo|drive|dubai|rehab|press|solar|sale|beer|bbva|bank|band|auto|sapo|sarl|saxo|audi|asia|arte|arpa|army|yoga|ally|zara|scor|scot|sexy|seat|zero|seek|aero|adac|zone|aarp|maif|meet|meme|menu|surf|mini|mobi|mtpc|porn|desi|star|ltda|name|talk|navy|love|loan|live|link|news|limo|like|spot|life|nico|lidl|lgbt|land|taxi|team|tech|kred|kpmg|sony|song|kiwi|kddi|jprs|jobs|sohu|java|itau|tips|info|immo|icbc|hsbc|town|host|page|toys|here|help|pars|haus|guru|guge|tube|goog|golf|gold|sncf|gmbh|gift|ggee|gent|gbiz|game|vana|pics|fund|ford|ping|pink|fish|film|fast|farm|play|fans|fail|plus|skin|pohl|fage|moda|post|erni|dvag|prod|doha|prof|docs|viva|diet|luxe|site|dell|sina|dclk|show|qpon|date|vote|cyou|voto|read|coop|cool|wang|club|city|chat|cern|cash|reit|rent|casa|cars|care|camp|rest|call|cafe|weir|wien|rich|wiki|buzz|wine|book|bond|room|work|rsvp|shia|ruhr|blue|bing|shaw|bike|safe|xbox|best|pwc|mtn|lds|aig|boo|fyi|nra|nrw|ntt|car|gal|obi|zip|aeg|vin|how|one|ong|onl|dad|ooo|bet|esq|org|htc|bar|uol|ibm|ovh|gdn|ice|icu|uno|gea|ifm|bot|top|wtf|lol|day|pet|eus|wtc|ubs|tvs|aco|ing|ltd|ink|tab|abb|afl|cat|int|pid|pin|bid|cba|gle|com|cbn|ads|man|wed|ceb|gmo|sky|ist|gmx|tui|mba|fan|ski|iwc|app|pro|med|ceo|jcb|jcp|goo|dev|men|aaa|meo|pub|jlc|bom|jll|gop|jmp|mil|got|gov|win|jot|mma|joy|trv|red|cfa|cfd|bio|moe|moi|mom|ren|biz|aws|xin|bbc|dnp|buy|kfh|mov|thd|xyz|fit|kia|rio|rip|kim|dog|vet|nyc|bcg|mtr|bcn|bms|bmw|run|bzh|rwe|tel|stc|axa|kpn|fly|krd|cab|bnl|foo|crs|eat|tci|sap|srl|nec|sas|net|cal|sbs|sfr|sca|scb|csc|edu|new|xxx|hiv|fox|wme|ngo|nhk|vip|sex|frl|lat|yun|law|you|tax|soy|sew|om|ac|hu|se|sc|sg|sh|sb|sa|rw|ru|rs|ro|re|qa|py|si|pw|pt|ps|sj|sk|pr|pn|pm|pl|sl|sm|pk|sn|ph|so|pg|pf|pe|pa|zw|nz|nu|nr|np|no|nl|ni|ng|nf|sr|ne|st|nc|na|mz|my|mx|mw|mv|mu|mt|ms|mr|mq|mp|mo|su|mn|mm|ml|mk|mh|mg|me|sv|md|mc|sx|sy|ma|ly|lv|sz|lu|lt|ls|lr|lk|li|lc|lb|la|tc|kz|td|ky|kw|kr|kp|kn|km|ki|kh|tf|tg|th|kg|ke|jp|jo|jm|je|it|is|ir|tj|tk|tl|tm|iq|tn|to|io|in|im|il|ie|ad|sd|ht|hr|hn|hm|tr|hk|gy|gw|gu|gt|gs|gr|gq|tt|gp|gn|gm|gl|tv|gi|tw|tz|ua|gh|ug|uk|gg|gf|ge|gd|us|uy|uz|va|gb|ga|vc|ve|fr|fo|fm|fk|fj|vg|vi|fi|eu|et|es|er|eg|ee|ec|dz|do|dm|dk|vn|dj|de|cz|cy|cx|cw|vu|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|wf|bz|by|bw|bv|bt|bs|br|bo|bn|bm|bj|bi|ws|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ye|ar|aq|ao|am|al|yt|ai|za|ag|af|ae|zm|id)\b/;return{alphaNumericCharsStr:r,domainNameRegex:a,tldRegex:n}}(),t.AnchorTagBuilder=t.Util.extend(Object,{constructor:function(e){t.Util.assign(this,e)},build:function(e){return new t.HtmlTag({tagName:"a",attrs:this.createAttrs(e.getType(),e.getAnchorHref()),innerHtml:this.processAnchorText(e.getAnchorText())})},createAttrs:function(t,e){var r={href:e},a=this.createCssClass(t);return a&&(r["class"]=a),this.newWindow&&(r.target="_blank",r.rel="noopener noreferrer"),r},createCssClass:function(t){var e=this.className;return e?e+" "+e+"-"+t:""},processAnchorText:function(t){return t=this.doTruncate(t)},doTruncate:function(e){var r=this.truncate;if(!r||!r.length)return e;var a=r.length,n=r.location;return"smart"===n?t.truncate.TruncateSmart(e,a,".."):"middle"===n?t.truncate.TruncateMiddle(e,a,".."):t.truncate.TruncateEnd(e,a,"..")}}),t.htmlParser.HtmlParser=t.Util.extend(Object,{htmlRegex:function(){var t=/!--([\s\S]+?)--/,e=/[0-9a-zA-Z][0-9a-zA-Z:]*/,r=/[^\s\0"'>\/=\x01-\x1F\x7F]+/,a=/(?:"[^"]*?"|'[^']*?'|[^'"=<>`\s]+)/,n=r.source+"(?:\\s*=\\s*"+a.source+")?";return new RegExp(["(?:","<(!DOCTYPE)","(?:","\\s+","(?:",n,"|",a.source+")",")*",">",")","|","(?:","<(/)?","(?:",t.source,"|","(?:","("+e.source+")","(?:","\\s*",n,")*","\\s*/?",")",")",">",")"].join(""),"gi")}(),htmlCharacterEntitiesRegex:/(&nbsp;|&#160;|&lt;|&#60;|&gt;|&#62;|&quot;|&#34;|&#39;)/gi,parse:function(t){for(var e,r,a=this.htmlRegex,n=0,i=[];null!==(e=a.exec(t));){var s=e[0],o=e[3],c=e[1]||e[4],h=!!e[2],l=e.index,u=t.substring(n,l);u&&(r=this.parseTextAndEntityNodes(n,u),i.push.apply(i,r)),o?i.push(this.createCommentNode(l,s,o)):i.push(this.createElementNode(l,s,c,h)),n=l+s.length}if(n<t.length){var g=t.substring(n);g&&(r=this.parseTextAndEntityNodes(n,g),i.push.apply(i,r))}return i},parseTextAndEntityNodes:function(e,r){for(var a=[],n=t.Util.splitAndCapture(r,this.htmlCharacterEntitiesRegex),i=0,s=n.length;s>i;i+=2){var o=n[i],c=n[i+1];o&&(a.push(this.createTextNode(e,o)),e+=o.length),c&&(a.push(this.createEntityNode(e,c)),e+=c.length)}return a},createCommentNode:function(e,r,a){return new t.htmlParser.CommentNode({offset:e,text:r,comment:t.Util.trim(a)})},createElementNode:function(e,r,a,n){return new t.htmlParser.ElementNode({offset:e,text:r,tagName:a.toLowerCase(),closing:n})},createEntityNode:function(e,r){return new t.htmlParser.EntityNode({offset:e,text:r})},createTextNode:function(e,r){return new t.htmlParser.TextNode({offset:e,text:r})}}),t.htmlParser.HtmlNode=t.Util.extend(Object,{offset:void 0,text:void 0,constructor:function(e){t.Util.assign(this,e)},getType:t.Util.abstractMethod,getOffset:function(){return this.offset},getText:function(){return this.text}}),t.htmlParser.CommentNode=t.Util.extend(t.htmlParser.HtmlNode,{comment:"",getType:function(){return"comment"},getComment:function(){return this.comment}}),t.htmlParser.ElementNode=t.Util.extend(t.htmlParser.HtmlNode,{tagName:"",closing:!1,getType:function(){return"element"},getTagName:function(){return this.tagName},isClosing:function(){return this.closing}}),t.htmlParser.EntityNode=t.Util.extend(t.htmlParser.HtmlNode,{getType:function(){return"entity"}}),t.htmlParser.TextNode=t.Util.extend(t.htmlParser.HtmlNode,{getType:function(){return"text"}}),t.match.Match=t.Util.extend(Object,{constructor:function(t){this.tagBuilder=t.tagBuilder,this.matchedText=t.matchedText,this.offset=t.offset},getType:t.Util.abstractMethod,getMatchedText:function(){return this.matchedText},setOffset:function(t){this.offset=t},getOffset:function(){return this.offset},getAnchorHref:t.Util.abstractMethod,getAnchorText:t.Util.abstractMethod,buildTag:function(){return this.tagBuilder.build(this)}}),t.match.Email=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.email=e.email},getType:function(){return"email"},getEmail:function(){return this.email},getAnchorHref:function(){return"mailto:"+this.email},getAnchorText:function(){return this.email}}),t.match.Hashtag=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.serviceName=e.serviceName,this.hashtag=e.hashtag},getType:function(){return"hashtag"},getServiceName:function(){return this.serviceName},getHashtag:function(){return this.hashtag},getAnchorHref:function(){var t=this.serviceName,e=this.hashtag;switch(t){case"twitter":return"https://twitter.com/hashtag/"+e;case"facebook":return"https://www.facebook.com/hashtag/"+e;case"instagram":return"https://instagram.com/explore/tags/"+e;default:throw new Error("Unknown service name to point hashtag to: ",t)}},getAnchorText:function(){return"#"+this.hashtag}}),t.match.Phone=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.number=e.number,this.plusSign=e.plusSign},getType:function(){return"phone"},getNumber:function(){return this.number},getAnchorHref:function(){return"tel:"+(this.plusSign?"+":"")+this.number},getAnchorText:function(){return this.matchedText}}),t.match.Twitter=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.twitterHandle=e.twitterHandle},getType:function(){return"twitter"},getTwitterHandle:function(){return this.twitterHandle},getAnchorHref:function(){return"https://twitter.com/"+this.twitterHandle},getAnchorText:function(){return"@"+this.twitterHandle}}),t.match.Url=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.urlMatchType=e.urlMatchType,this.url=e.url,this.protocolUrlMatch=e.protocolUrlMatch,this.protocolRelativeMatch=e.protocolRelativeMatch,this.stripPrefix=e.stripPrefix},urlPrefixRegex:/^(https?:\/\/)?(www\.)?/i,protocolRelativeRegex:/^\/\//,protocolPrepended:!1,getType:function(){return"url"},getUrlMatchType:function(){return this.urlMatchType},getUrl:function(){var t=this.url;return this.protocolRelativeMatch||this.protocolUrlMatch||this.protocolPrepended||(t=this.url="http://"+t,this.protocolPrepended=!0),t},getAnchorHref:function(){var t=this.getUrl();return t.replace(/&amp;/g,"&")},getAnchorText:function(){var t=this.getMatchedText();return this.protocolRelativeMatch&&(t=this.stripProtocolRelativePrefix(t)),this.stripPrefix&&(t=this.stripUrlPrefix(t)),t=this.removeTrailingSlash(t)},stripUrlPrefix:function(t){return t.replace(this.urlPrefixRegex,"")},stripProtocolRelativePrefix:function(t){return t.replace(this.protocolRelativeRegex,"")},removeTrailingSlash:function(t){return"/"===t.charAt(t.length-1)&&(t=t.slice(0,-1)),t}}),t.matcher.Matcher=t.Util.extend(Object,{constructor:function(t){this.tagBuilder=t.tagBuilder},parseMatches:t.Util.abstractMethod}),t.matcher.Email=t.Util.extend(t.matcher.Matcher,{matcherRegex:function(){var e=t.RegexLib.alphaNumericCharsStr,r=new RegExp("["+e+"\\-;:&=+$.,]+@"),a=t.RegexLib.domainNameRegex,n=t.RegexLib.tldRegex;return new RegExp([r.source,a.source,"\\.",n.source].join(""),"gi")}(),parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.tagBuilder,i=[];null!==(r=a.exec(e));){var s=r[0];i.push(new t.match.Email({tagBuilder:n,matchedText:s,offset:r.index,email:s}))}return i}}),t.matcher.Hashtag=t.Util.extend(t.matcher.Matcher,{matcherRegex:new RegExp("#[_"+t.RegexLib.alphaNumericCharsStr+"]{1,139}","g"),nonWordCharRegex:new RegExp("[^"+t.RegexLib.alphaNumericCharsStr+"]"),constructor:function(e){t.matcher.Matcher.prototype.constructor.call(this,e),this.serviceName=e.serviceName},parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.nonWordCharRegex,i=this.serviceName,s=this.tagBuilder,o=[];null!==(r=a.exec(e));){var c=r.index,h=e.charAt(c-1);if(0===c||n.test(h)){var l=r[0],u=r[0].slice(1);o.push(new t.match.Hashtag({tagBuilder:s,matchedText:l,offset:c,serviceName:i,hashtag:u}))}}return o}}),t.matcher.Phone=t.Util.extend(t.matcher.Matcher,{matcherRegex:/(?:(\+)?\d{1,3}[-\040.])?\(?\d{3}\)?[-\040.]?\d{3}[-\040.]\d{4}/g,parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.tagBuilder,i=[];null!==(r=a.exec(e));){var s=r[0],o=s.replace(/\D/g,""),c=!!r[1];i.push(new t.match.Phone({tagBuilder:n,matchedText:s,offset:r.index,number:o,plusSign:c}))}return i}}),t.matcher.Twitter=t.Util.extend(t.matcher.Matcher,{matcherRegex:new RegExp("@[_"+t.RegexLib.alphaNumericCharsStr+"]{1,20}","g"),nonWordCharRegex:new RegExp("[^"+t.RegexLib.alphaNumericCharsStr+"]"),parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.nonWordCharRegex,i=this.tagBuilder,s=[];null!==(r=a.exec(e));){var o=r.index,c=e.charAt(o-1);if(0===o||n.test(c)){var h=r[0],l=r[0].slice(1);s.push(new t.match.Twitter({tagBuilder:i,matchedText:h,offset:o,twitterHandle:l}))}}return s}}),t.matcher.Url=t.Util.extend(t.matcher.Matcher,{matcherRegex:function(){var e=/(?:[A-Za-z][-.+A-Za-z0-9]*:(?![A-Za-z][-.+A-Za-z0-9]*:\/\/)(?!\d+\/?)(?:\/\/)?)/,r=/(?:www\.)/,a=t.RegexLib.domainNameRegex,n=t.RegexLib.tldRegex,i=t.RegexLib.alphaNumericCharsStr,s=new RegExp("["+i+"\\-+&@#/%=~_()|'$*\\[\\]?!:,.;]*["+i+"\\-+&@#/%=~_()|'$*\\[\\]]");return new RegExp(["(?:","(",e.source,a.source,")","|","(","(//)?",r.source,a.source,")","|","(","(//)?",a.source+"\\.",n.source,")",")","(?:"+s.source+")?"].join(""),"gi")}(),wordCharRegExp:/\w/,openParensRe:/\(/g,closeParensRe:/\)/g,constructor:function(e){t.matcher.Matcher.prototype.constructor.call(this,e),this.stripPrefix=e.stripPrefix},parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.stripPrefix,i=this.tagBuilder,s=[];null!==(r=a.exec(e));){var o=r[0],c=r[1],h=r[2],l=r[3],u=r[5],g=r.index,m=l||u,f=e.charAt(g-1);if(t.matcher.UrlMatchValidator.isValid(o,c)&&!(g>0&&"@"===f||g>0&&m&&this.wordCharRegExp.test(f))){if(this.matchHasUnbalancedClosingParen(o))o=o.substr(0,o.length-1);else{var p=this.matchHasInvalidCharAfterTld(o,c);p>-1&&(o=o.substr(0,p))}var d=c?"scheme":h?"www":"tld",b=!!c;s.push(new t.match.Url({tagBuilder:i,matchedText:o,offset:g,urlMatchType:d,url:o,protocolUrlMatch:b,protocolRelativeMatch:!!m,stripPrefix:n}))}}return s},matchHasUnbalancedClosingParen:function(t){var e=t.charAt(t.length-1);if(")"===e){var r=t.match(this.openParensRe),a=t.match(this.closeParensRe),n=r&&r.length||0,i=a&&a.length||0;if(i>n)return!0}return!1},matchHasInvalidCharAfterTld:function(t,e){if(!t)return-1;var r=0;e&&(r=t.indexOf(":"),t=t.slice(r));var a=/^((.?\/\/)?[A-Za-z0-9\u00C0-\u017F\.\-]*[A-Za-z0-9\u00C0-\u017F\-]\.[A-Za-z]+)/,n=a.exec(t);return null===n?-1:(r+=n[1].length,t=t.slice(n[1].length),/^[^.A-Za-z:\/?#]/.test(t)?r:-1)}}),t.matcher.UrlMatchValidator={hasFullProtocolRegex:/^[A-Za-z][-.+A-Za-z0-9]*:\/\//,uriSchemeRegex:/^[A-Za-z][-.+A-Za-z0-9]*:/,hasWordCharAfterProtocolRegex:/:[^\s]*?[A-Za-z\u00C0-\u017F]/,isValid:function(t,e){return!(e&&!this.isValidUriScheme(e)||this.urlMatchDoesNotHaveProtocolOrDot(t,e)||this.urlMatchDoesNotHaveAtLeastOneWordChar(t,e))},isValidUriScheme:function(t){var e=t.match(this.uriSchemeRegex)[0].toLowerCase();return"javascript:"!==e&&"vbscript:"!==e},urlMatchDoesNotHaveProtocolOrDot:function(t,e){return!(!t||e&&this.hasFullProtocolRegex.test(e)||-1!==t.indexOf("."))},urlMatchDoesNotHaveAtLeastOneWordChar:function(t,e){return t&&e?!this.hasWordCharAfterProtocolRegex.test(t):!1}},t.truncate.TruncateEnd=function(e,r,a){return t.Util.ellipsis(e,r,a)},t.truncate.TruncateMiddle=function(t,e,r){if(t.length<=e)return t;var a=e-r.length,n="";return a>0&&(n=t.substr(-1*Math.floor(a/2))),(t.substr(0,Math.ceil(a/2))+r+n).substr(0,e)},t.truncate.TruncateSmart=function(t,e,r){var a=function(t){var e={},r=t,a=r.match(/^([a-z]+):\/\//i);return a&&(e.scheme=a[1],r=r.substr(a[0].length)),a=r.match(/^(.*?)(?=(\?|#|\/|$))/i),a&&(e.host=a[1],r=r.substr(a[0].length)),a=r.match(/^\/(.*?)(?=(\?|#|$))/i),a&&(e.path=a[1],r=r.substr(a[0].length)),a=r.match(/^\?(.*?)(?=(#|$))/i),a&&(e.query=a[1],r=r.substr(a[0].length)),a=r.match(/^#(.*?)$/i),a&&(e.fragment=a[1]),e},n=function(t){var e="";return t.scheme&&t.host&&(e+=t.scheme+"://"),t.host&&(e+=t.host),t.path&&(e+="/"+t.path),t.query&&(e+="?"+t.query),t.fragment&&(e+="#"+t.fragment),e},i=function(t,e){var a=e/2,n=Math.ceil(a),i=-1*Math.floor(a),s="";return 0>i&&(s=t.substr(i)),t.substr(0,n)+r+s};if(t.length<=e)return t;var s=e-r.length,o=a(t);if(o.query){var c=o.query.match(/^(.*?)(?=(\?|\#))(.*?)$/i);c&&(o.query=o.query.substr(0,c[1].length),t=n(o))}if(t.length<=e)return t;if(o.host&&(o.host=o.host.replace(/^www\./,""),t=n(o)),t.length<=e)return t;var h="";if(o.host&&(h+=o.host),h.length>=s)return o.host.length==e?(o.host.substr(0,e-r.length)+r).substr(0,e):i(h,s).substr(0,e);var l="";if(o.path&&(l+="/"+o.path),o.query&&(l+="?"+o.query),l){if((h+l).length>=s){if((h+l).length==e)return(h+l).substr(0,e);var u=s-h.length;return(h+i(l,u)).substr(0,e)}h+=l}if(o.fragment){var g="#"+o.fragment;if((h+g).length>=s){if((h+g).length==e)return(h+g).substr(0,e);var m=s-h.length;return(h+i(g,m)).substr(0,e)}h+=g}if(o.scheme&&o.host){var f=o.scheme+"://";if((h+f).length<s)return(f+h).substr(0,e)}if(h.length<=e)return h;var p="";return s>0&&(p=h.substr(-1*Math.floor(s/2))),(h.substr(0,Math.ceil(s/2))+r+p).substr(0,e)},t});
\ No newline at end of file
+!function(t,e){"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?module.exports=e():t.Autolinker=e()}(this,function(){var t=function(e){e=e||{},this.version=t.version,this.urls=this.normalizeUrlsCfg(e.urls),this.email="boolean"!=typeof e.email||e.email,this.twitter="boolean"!=typeof e.twitter||e.twitter,this.phone="boolean"!=typeof e.phone||e.phone,this.hashtag=e.hashtag||!1,this.newWindow="boolean"!=typeof e.newWindow||e.newWindow,this.stripPrefix="boolean"!=typeof e.stripPrefix||e.stripPrefix;var r=this.hashtag;if(r!==!1&&"twitter"!==r&&"facebook"!==r&&"instagram"!==r)throw new Error("invalid `hashtag` cfg - see docs");this.truncate=this.normalizeTruncateCfg(e.truncate),this.className=e.className||"",this.replaceFn=e.replaceFn||null,this.htmlParser=null,this.matchers=null,this.tagBuilder=null};return t.link=function(e,r){var a=new t(r);return a.link(e)},t.version="0.28.0",t.prototype={constructor:t,normalizeUrlsCfg:function(t){return null==t&&(t=!0),"boolean"==typeof t?{schemeMatches:t,wwwMatches:t,tldMatches:t}:{schemeMatches:"boolean"!=typeof t.schemeMatches||t.schemeMatches,wwwMatches:"boolean"!=typeof t.wwwMatches||t.wwwMatches,tldMatches:"boolean"!=typeof t.tldMatches||t.tldMatches}},normalizeTruncateCfg:function(e){return"number"==typeof e?{length:e,location:"end"}:t.Util.defaults(e||{},{length:Number.POSITIVE_INFINITY,location:"end"})},parse:function(t){for(var e=this.getHtmlParser(),r=e.parse(t),a=0,n=[],i=0,s=r.length;i<s;i++){var o=r[i],c=o.getType();if("element"===c&&"a"===o.getTagName())o.isClosing()?a=Math.max(a-1,0):a++;else if("text"===c&&0===a){var h=this.parseText(o.getText(),o.getOffset());n.push.apply(n,h)}}return n=this.compactMatches(n),n=this.removeUnwantedMatches(n)},compactMatches:function(t){t.sort(function(t,e){return t.getOffset()-e.getOffset()});for(var e=0;e<t.length-1;e++)for(var r=t[e],a=r.getOffset()+r.getMatchedText().length;e+1<t.length&&t[e+1].getOffset()<=a;)t.splice(e+1,1);return t},removeUnwantedMatches:function(e){var r=t.Util.remove;return this.hashtag||r(e,function(t){return"hashtag"===t.getType()}),this.email||r(e,function(t){return"email"===t.getType()}),this.phone||r(e,function(t){return"phone"===t.getType()}),this.twitter||r(e,function(t){return"twitter"===t.getType()}),this.urls.schemeMatches||r(e,function(t){return"url"===t.getType()&&"scheme"===t.getUrlMatchType()}),this.urls.wwwMatches||r(e,function(t){return"url"===t.getType()&&"www"===t.getUrlMatchType()}),this.urls.tldMatches||r(e,function(t){return"url"===t.getType()&&"tld"===t.getUrlMatchType()}),e},parseText:function(t,e){e=e||0;for(var r=this.getMatchers(),a=[],n=0,i=r.length;n<i;n++){for(var s=r[n].parseMatches(t),o=0,c=s.length;o<c;o++)s[o].setOffset(e+s[o].getOffset());a.push.apply(a,s)}return a},link:function(t){if(!t)return"";for(var e=this.parse(t),r=[],a=0,n=0,i=e.length;n<i;n++){var s=e[n];r.push(t.substring(a,s.getOffset())),r.push(this.createMatchReturnVal(s)),a=s.getOffset()+s.getMatchedText().length}return r.push(t.substring(a)),r.join("")},createMatchReturnVal:function(e){var r;if(this.replaceFn&&(r=this.replaceFn.call(this,this,e)),"string"==typeof r)return r;if(r===!1)return e.getMatchedText();if(r instanceof t.HtmlTag)return r.toAnchorString();var a=e.buildTag();return a.toAnchorString()},getHtmlParser:function(){var e=this.htmlParser;return e||(e=this.htmlParser=new t.htmlParser.HtmlParser),e},getMatchers:function(){if(this.matchers)return this.matchers;var e=t.matcher,r=this.getTagBuilder(),a=[new e.Hashtag({tagBuilder:r,serviceName:this.hashtag}),new e.Email({tagBuilder:r}),new e.Phone({tagBuilder:r}),new e.Twitter({tagBuilder:r}),new e.Url({tagBuilder:r,stripPrefix:this.stripPrefix})];return this.matchers=a},getTagBuilder:function(){var e=this.tagBuilder;return e||(e=this.tagBuilder=new t.AnchorTagBuilder({newWindow:this.newWindow,truncate:this.truncate,className:this.className})),e}},t.match={},t.matcher={},t.htmlParser={},t.truncate={},t.Util={abstractMethod:function(){throw"abstract"},trimRegex:/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,assign:function(t,e){for(var r in e)e.hasOwnProperty(r)&&(t[r]=e[r]);return t},defaults:function(t,e){for(var r in e)e.hasOwnProperty(r)&&void 0===t[r]&&(t[r]=e[r]);return t},extend:function(e,r){var a=e.prototype,n=function(){};n.prototype=a;var i;i=r.hasOwnProperty("constructor")?r.constructor:function(){a.constructor.apply(this,arguments)};var s=i.prototype=new n;return s.constructor=i,s.superclass=a,delete r.constructor,t.Util.assign(s,r),i},ellipsis:function(t,e,r){return t.length>e&&(r=null==r?"..":r,t=t.substring(0,e-r.length)+r),t},indexOf:function(t,e){if(Array.prototype.indexOf)return t.indexOf(e);for(var r=0,a=t.length;r<a;r++)if(t[r]===e)return r;return-1},remove:function(t,e){for(var r=t.length-1;r>=0;r--)e(t[r])===!0&&t.splice(r,1)},splitAndCapture:function(t,e){for(var r,a=[],n=0;r=e.exec(t);)a.push(t.substring(n,r.index)),a.push(r[0]),n=r.index+r[0].length;return a.push(t.substring(n)),a},trim:function(t){return t.replace(this.trimRegex,"")}},t.HtmlTag=t.Util.extend(Object,{whitespaceRegex:/\s+/,constructor:function(e){t.Util.assign(this,e),this.innerHtml=this.innerHtml||this.innerHTML},setTagName:function(t){return this.tagName=t,this},getTagName:function(){return this.tagName||""},setAttr:function(t,e){var r=this.getAttrs();return r[t]=e,this},getAttr:function(t){return this.getAttrs()[t]},setAttrs:function(e){var r=this.getAttrs();return t.Util.assign(r,e),this},getAttrs:function(){return this.attrs||(this.attrs={})},setClass:function(t){return this.setAttr("class",t)},addClass:function(e){for(var r,a=this.getClass(),n=this.whitespaceRegex,i=t.Util.indexOf,s=a?a.split(n):[],o=e.split(n);r=o.shift();)i(s,r)===-1&&s.push(r);return this.getAttrs()["class"]=s.join(" "),this},removeClass:function(e){for(var r,a=this.getClass(),n=this.whitespaceRegex,i=t.Util.indexOf,s=a?a.split(n):[],o=e.split(n);s.length&&(r=o.shift());){var c=i(s,r);c!==-1&&s.splice(c,1)}return this.getAttrs()["class"]=s.join(" "),this},getClass:function(){return this.getAttrs()["class"]||""},hasClass:function(t){return(" "+this.getClass()+" ").indexOf(" "+t+" ")!==-1},setInnerHtml:function(t){return this.innerHtml=t,this},getInnerHtml:function(){return this.innerHtml||""},toAnchorString:function(){var t=this.getTagName(),e=this.buildAttrsStr();return e=e?" "+e:"",["<",t,e,">",this.getInnerHtml(),"</",t,">"].join("")},buildAttrsStr:function(){if(!this.attrs)return"";var t=this.getAttrs(),e=[];for(var r in t)t.hasOwnProperty(r)&&e.push(r+'="'+t[r]+'"');return e.join(" ")}}),t.RegexLib=function(){var t="A-Za-z\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-ˁˆ-ˑˠ-ˤˬˮͰ-ʹͶͷͺ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԯԱ-Ֆՙա-ևא-תװ-ײؠ-يٮٯٱ-ۓەۥۦۮۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪߴߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࢠ-ࢴऄ-हऽॐक़-ॡॱ-ঀঅ-ঌএঐও-নপ-রলশ-হঽৎড়ঢ়য়-ৡৰৱਅ-ਊਏਐਓ-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલળવ-હઽૐૠૡૹଅ-ଌଏଐଓ-ନପ-ରଲଳଵ-ହଽଡ଼ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கஙசஜஞடணதந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-హఽౘ-ౚౠౡಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-ഐഒ-ഺഽൎൟ-ൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะาำเ-ๆກຂຄງຈຊຍດ-ທນ-ຟມ-ຣລວສຫອ-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿၐ-ၕၚ-ၝၡၥၦၮ-ၰၵ-ႁႎႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏽᏸ-ᏽᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᛱ-ᛸᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៗៜᠠ-ᡷᢀ-ᢨᢪᢰ-ᣵᤀ-ᤞᥐ-ᥭᥰ-ᥴᦀ-ᦫᦰ-ᧉᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮᮯᮺ-ᯥᰀ-ᰣᱍ-ᱏᱚ-ᱽᳩ-ᳬᳮ-ᳱᳵᳶᴀ-ᶿḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼⁱⁿₐ-ₜℂℇℊ-ℓℕℙ-ℝℤΩℨK-ℭℯ-ℹℼ-ℿⅅ-ⅉⅎↃↄⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞⸯ々〆〱-〵〻〼ぁ-ゖゝ-ゟァ-ヺー-ヿㄅ-ㄭㄱ-ㆎㆠ-ㆺㇰ-ㇿ㐀-䶵一-鿕ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘟꘪꘫꙀ-ꙮꙿ-ꚝꚠ-ꛥꜗ-ꜟꜢ-ꞈꞋ-ꞭꞰ-ꞷꟷ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꣽꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲꧏꧠ-ꧤꧦ-ꧯꧺ-ꧾꨀ-ꨨꩀ-ꩂꩄ-ꩋꩠ-ꩶꩺꩾ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ꫝꫠ-ꫪꫲ-ꫴꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꬰ-ꭚꭜ-ꭥꭰ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ff-stﬓ-ﬗיִײַ-ﬨשׁ-זּטּ-לּמּנּסּףּפּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼA-Za-zヲ-하-ᅦᅧ-ᅬᅭ-ᅲᅳ-ᅵ",e="0-9٠-٩۰-۹߀-߉०-९০-৯੦-੯૦-૯୦-୯௦-௯౦-౯೦-೯൦-൯෦-෯๐-๙໐-໙༠-༩၀-၉႐-႙០-៩᠐-᠙᥆-᥏᧐-᧙᪀-᪉᪐-᪙᭐-᭙᮰-᮹᱀-᱉᱐-᱙꘠-꘩꣐-꣙꤀-꤉꧐-꧙꧰-꧹꩐-꩙꯰-꯹0-9",r=t+e,a=new RegExp("["+r+".\\-]*["+r+"\\-]"),n=/(?:travelersinsurance|sandvikcoromant|kerryproperties|cancerresearch|weatherchannel|kerrylogistics|spreadbetting|international|wolterskluwer|lifeinsurance|construction|pamperedchef|scholarships|versicherung|bridgestone|creditunion|kerryhotels|investments|productions|blackfriday|enterprises|lamborghini|photography|motorcycles|williamhill|playstation|contractors|barclaycard|accountants|redumbrella|engineering|management|telefonica|protection|consulting|tatamotors|creditcard|vlaanderen|schaeffler|associates|properties|foundation|republican|bnpparibas|boehringer|eurovision|extraspace|industries|immobilien|university|technology|volkswagen|healthcare|restaurant|cuisinella|vistaprint|apartments|accountant|travelers|homedepot|institute|vacations|furniture|fresenius|insurance|christmas|bloomberg|solutions|barcelona|firestone|financial|kuokgroup|fairwinds|community|passagens|goldpoint|equipment|lifestyle|yodobashi|aquarelle|marketing|analytics|education|amsterdam|statefarm|melbourne|allfinanz|directory|microsoft|stockholm|montblanc|accenture|lancaster|landrover|everbank|istanbul|graphics|grainger|ipiranga|softbank|attorney|pharmacy|saarland|catering|airforce|yokohama|mortgage|frontier|mutuelle|stcgroup|memorial|pictures|football|symantec|cipriani|ventures|telecity|cityeats|verisign|flsmidth|boutique|cleaning|firmdale|clinique|clothing|redstone|infiniti|deloitte|feedback|services|broadway|plumbing|commbank|training|barclays|exchange|computer|brussels|software|delivery|barefoot|builders|business|bargains|engineer|holdings|download|security|helsinki|lighting|movistar|discount|hdfcbank|supplies|marriott|property|diamonds|capetown|partners|democrat|jpmorgan|bradesco|budapest|rexroth|zuerich|shriram|academy|science|support|youtube|singles|surgery|alibaba|statoil|dentist|schwarz|android|cruises|cricket|digital|markets|starhub|systems|courses|coupons|netbank|country|domains|corsica|network|neustar|realtor|lincoln|limited|schmidt|yamaxun|cooking|contact|auction|spiegel|liaison|leclerc|latrobe|lasalle|abogado|compare|lanxess|exposed|express|company|cologne|college|avianca|lacaixa|fashion|recipes|ferrero|komatsu|storage|wanggou|clubmed|sandvik|fishing|fitness|bauhaus|kitchen|flights|florist|flowers|watches|weather|temasek|samsung|bentley|forsale|channel|theater|frogans|theatre|okinawa|website|tickets|jewelry|gallery|tiffany|iselect|shiksha|brother|organic|wedding|genting|toshiba|origins|philips|hyundai|hotmail|hoteles|hosting|rentals|windows|cartier|bugatti|holiday|careers|whoswho|hitachi|panerai|caravan|reviews|guitars|capital|trading|hamburg|hangout|finance|stream|family|abbott|health|review|travel|report|hermes|hiphop|gratis|career|toyota|hockey|dating|repair|google|social|soccer|reisen|global|otsuka|giving|unicom|casino|photos|center|broker|rocher|orange|bostik|garden|insure|ryukyu|bharti|safety|physio|sakura|oracle|online|jaguar|gallup|piaget|tienda|futbol|pictet|joburg|webcam|berlin|office|juegos|kaufen|chanel|chrome|xihuan|church|tennis|circle|kinder|flickr|bayern|claims|clinic|viajes|nowruz|xperia|norton|yachts|studio|coffee|camera|sanofi|nissan|author|expert|events|comsec|lawyer|tattoo|viking|estate|villas|condos|realty|yandex|energy|emerck|virgin|vision|durban|living|school|coupon|london|taobao|natura|taipei|nagoya|luxury|walter|aramco|sydney|madrid|credit|maison|makeup|schule|market|anquan|direct|design|swatch|suzuki|alsace|vuelos|dental|alipay|voyage|shouji|voting|airtel|mutual|degree|supply|agency|museum|mobily|dealer|monash|select|mormon|active|moscow|racing|datsun|quebec|nissay|rodeo|email|gifts|works|photo|chloe|edeka|cheap|earth|vista|tushu|koeln|glass|shoes|globo|tunes|gmail|nokia|space|kyoto|black|ricoh|seven|lamer|sener|epson|cisco|praxi|trust|citic|crown|shell|lease|green|legal|lexus|ninja|tatar|gripe|nikon|group|video|wales|autos|gucci|party|nexus|guide|linde|adult|parts|amica|lixil|boats|azure|loans|locus|cymru|lotte|lotto|stada|click|poker|quest|dabur|lupin|nadex|paris|faith|dance|canon|place|gives|trade|skype|rocks|mango|cloud|boots|smile|final|swiss|homes|honda|media|horse|cards|deals|watch|bosch|house|pizza|miami|osaka|tours|total|xerox|coach|sucks|style|delta|toray|iinet|tools|money|codes|beats|tokyo|salon|archi|movie|baidu|study|actor|yahoo|store|apple|world|forex|today|bible|tmall|tirol|irish|tires|forum|reise|vegas|vodka|sharp|omega|weber|jetzt|audio|promo|build|bingo|chase|gallo|drive|dubai|rehab|press|solar|sale|beer|bbva|bank|band|auto|sapo|sarl|saxo|audi|asia|arte|arpa|army|yoga|ally|zara|scor|scot|sexy|seat|zero|seek|aero|adac|zone|aarp|maif|meet|meme|menu|surf|mini|mobi|mtpc|porn|desi|star|ltda|name|talk|navy|love|loan|live|link|news|limo|like|spot|life|nico|lidl|lgbt|land|taxi|team|tech|kred|kpmg|sony|song|kiwi|kddi|jprs|jobs|sohu|java|itau|tips|info|immo|icbc|hsbc|town|host|page|toys|here|help|pars|haus|guru|guge|tube|goog|golf|gold|sncf|gmbh|gift|ggee|gent|gbiz|game|vana|pics|fund|ford|ping|pink|fish|film|fast|farm|play|fans|fail|plus|skin|pohl|fage|moda|post|erni|dvag|prod|doha|prof|docs|viva|diet|luxe|site|dell|sina|dclk|show|qpon|date|vote|cyou|voto|read|coop|cool|wang|club|city|chat|cern|cash|reit|rent|casa|cars|care|camp|rest|call|cafe|weir|wien|rich|wiki|buzz|wine|book|bond|room|work|rsvp|shia|ruhr|blue|bing|shaw|bike|safe|xbox|best|pwc|mtn|lds|aig|boo|fyi|nra|nrw|ntt|car|gal|obi|zip|aeg|vin|how|one|ong|onl|dad|ooo|bet|esq|org|htc|bar|uol|ibm|ovh|gdn|ice|icu|uno|gea|ifm|bot|top|wtf|lol|day|pet|eus|wtc|ubs|tvs|aco|ing|ltd|ink|tab|abb|afl|cat|int|pid|pin|bid|cba|gle|com|cbn|ads|man|wed|ceb|gmo|sky|ist|gmx|tui|mba|fan|ski|iwc|app|pro|med|ceo|jcb|jcp|goo|dev|men|aaa|meo|pub|jlc|bom|jll|gop|jmp|mil|got|gov|win|jot|mma|joy|trv|red|cfa|cfd|bio|moe|moi|mom|ren|biz|aws|xin|bbc|dnp|buy|kfh|mov|thd|xyz|fit|kia|rio|rip|kim|dog|vet|nyc|bcg|mtr|bcn|bms|bmw|run|bzh|rwe|tel|stc|axa|kpn|fly|krd|cab|bnl|foo|crs|eat|tci|sap|srl|nec|sas|net|cal|sbs|sfr|sca|scb|csc|edu|new|xxx|hiv|fox|wme|ngo|nhk|vip|sex|frl|lat|yun|law|you|tax|soy|sew|om|ac|hu|se|sc|sg|sh|sb|sa|rw|ru|rs|ro|re|qa|py|si|pw|pt|ps|sj|sk|pr|pn|pm|pl|sl|sm|pk|sn|ph|so|pg|pf|pe|pa|zw|nz|nu|nr|np|no|nl|ni|ng|nf|sr|ne|st|nc|na|mz|my|mx|mw|mv|mu|mt|ms|mr|mq|mp|mo|su|mn|mm|ml|mk|mh|mg|me|sv|md|mc|sx|sy|ma|ly|lv|sz|lu|lt|ls|lr|lk|li|lc|lb|la|tc|kz|td|ky|kw|kr|kp|kn|km|ki|kh|tf|tg|th|kg|ke|jp|jo|jm|je|it|is|ir|tj|tk|tl|tm|iq|tn|to|io|in|im|il|ie|ad|sd|ht|hr|hn|hm|tr|hk|gy|gw|gu|gt|gs|gr|gq|tt|gp|gn|gm|gl|tv|gi|tw|tz|ua|gh|ug|uk|gg|gf|ge|gd|us|uy|uz|va|gb|ga|vc|ve|fr|fo|fm|fk|fj|vg|vi|fi|eu|et|es|er|eg|ee|ec|dz|do|dm|dk|vn|dj|de|cz|cy|cx|cw|vu|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|wf|bz|by|bw|bv|bt|bs|br|bo|bn|bm|bj|bi|ws|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ye|ar|aq|ao|am|al|yt|ai|za|ag|af|ae|zm|id)\b/;return{alphaNumericCharsStr:r,domainNameRegex:a,tldRegex:n}}(),t.AnchorTagBuilder=t.Util.extend(Object,{constructor:function(e){t.Util.assign(this,e)},build:function(e){return new t.HtmlTag({tagName:"a",attrs:this.createAttrs(e.getType(),e.getAnchorHref()),innerHtml:this.processAnchorText(e.getAnchorText())})},createAttrs:function(t,e){var r={href:e},a=this.createCssClass(t);return a&&(r["class"]=a),this.newWindow&&(r.target="_blank",r.rel="noopener noreferrer"),r},createCssClass:function(t){var e=this.className;return e?e+" "+e+"-"+t:""},processAnchorText:function(t){return t=this.doTruncate(t)},doTruncate:function(e){var r=this.truncate;if(!r||!r.length)return e;var a=r.length,n=r.location;return"smart"===n?t.truncate.TruncateSmart(e,a,".."):"middle"===n?t.truncate.TruncateMiddle(e,a,".."):t.truncate.TruncateEnd(e,a,"..")}}),t.htmlParser.HtmlParser=t.Util.extend(Object,{htmlRegex:function(){var t=/!--([\s\S]+?)--/,e=/[0-9a-zA-Z][0-9a-zA-Z:]*/,r=/[^\s"'>\/=\x00-\x1F\x7F]+/,a=/(?:"[^"]*?"|'[^']*?'|[^'"=<>`\s]+)/,n=r.source+"(?:\\s*=\\s*"+a.source+")?";return new RegExp(["(?:","<(!DOCTYPE)","(?:","\\s+","(?:",n,"|",a.source+")",")*",">",")","|","(?:","<(/)?","(?:",t.source,"|","(?:","("+e.source+")","(?:","(?:\\s+|\\b)",n,")*","\\s*/?",")",")",">",")"].join(""),"gi")}(),htmlCharacterEntitiesRegex:/(&nbsp;|&#160;|&lt;|&#60;|&gt;|&#62;|&quot;|&#34;|&#39;)/gi,parse:function(t){for(var e,r,a=this.htmlRegex,n=0,i=[];null!==(e=a.exec(t));){var s=e[0],o=e[3],c=e[1]||e[4],h=!!e[2],l=e.index,u=t.substring(n,l);u&&(r=this.parseTextAndEntityNodes(n,u),i.push.apply(i,r)),o?i.push(this.createCommentNode(l,s,o)):i.push(this.createElementNode(l,s,c,h)),n=l+s.length}if(n<t.length){var g=t.substring(n);g&&(r=this.parseTextAndEntityNodes(n,g),i.push.apply(i,r))}return i},parseTextAndEntityNodes:function(e,r){for(var a=[],n=t.Util.splitAndCapture(r,this.htmlCharacterEntitiesRegex),i=0,s=n.length;i<s;i+=2){var o=n[i],c=n[i+1];o&&(a.push(this.createTextNode(e,o)),e+=o.length),c&&(a.push(this.createEntityNode(e,c)),e+=c.length)}return a},createCommentNode:function(e,r,a){return new t.htmlParser.CommentNode({offset:e,text:r,comment:t.Util.trim(a)})},createElementNode:function(e,r,a,n){return new t.htmlParser.ElementNode({offset:e,text:r,tagName:a.toLowerCase(),closing:n})},createEntityNode:function(e,r){return new t.htmlParser.EntityNode({offset:e,text:r})},createTextNode:function(e,r){return new t.htmlParser.TextNode({offset:e,text:r})}}),t.htmlParser.HtmlNode=t.Util.extend(Object,{offset:void 0,text:void 0,constructor:function(e){t.Util.assign(this,e)},getType:t.Util.abstractMethod,getOffset:function(){return this.offset},getText:function(){return this.text}}),t.htmlParser.CommentNode=t.Util.extend(t.htmlParser.HtmlNode,{comment:"",getType:function(){return"comment"},getComment:function(){return this.comment}}),t.htmlParser.ElementNode=t.Util.extend(t.htmlParser.HtmlNode,{tagName:"",closing:!1,getType:function(){return"element"},getTagName:function(){return this.tagName},isClosing:function(){return this.closing}}),t.htmlParser.EntityNode=t.Util.extend(t.htmlParser.HtmlNode,{getType:function(){return"entity"}}),t.htmlParser.TextNode=t.Util.extend(t.htmlParser.HtmlNode,{getType:function(){return"text"}}),t.match.Match=t.Util.extend(Object,{constructor:function(t){this.tagBuilder=t.tagBuilder,this.matchedText=t.matchedText,this.offset=t.offset},getType:t.Util.abstractMethod,getMatchedText:function(){return this.matchedText},setOffset:function(t){this.offset=t},getOffset:function(){return this.offset},getAnchorHref:t.Util.abstractMethod,getAnchorText:t.Util.abstractMethod,buildTag:function(){return this.tagBuilder.build(this)}}),t.match.Email=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.email=e.email},getType:function(){return"email"},getEmail:function(){return this.email},getAnchorHref:function(){return"mailto:"+this.email},getAnchorText:function(){return this.email}}),t.match.Hashtag=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.serviceName=e.serviceName,this.hashtag=e.hashtag},getType:function(){return"hashtag"},getServiceName:function(){return this.serviceName},getHashtag:function(){return this.hashtag},getAnchorHref:function(){var t=this.serviceName,e=this.hashtag;switch(t){case"twitter":return"https://twitter.com/hashtag/"+e;case"facebook":return"https://www.facebook.com/hashtag/"+e;case"instagram":return"https://instagram.com/explore/tags/"+e;default:throw new Error("Unknown service name to point hashtag to: ",t)}},getAnchorText:function(){return"#"+this.hashtag}}),t.match.Phone=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.number=e.number,this.plusSign=e.plusSign},getType:function(){return"phone"},getNumber:function(){return this.number},getAnchorHref:function(){return"tel:"+(this.plusSign?"+":"")+this.number},getAnchorText:function(){return this.matchedText}}),t.match.Twitter=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.twitterHandle=e.twitterHandle},getType:function(){return"twitter"},getTwitterHandle:function(){return this.twitterHandle},getAnchorHref:function(){return"https://twitter.com/"+this.twitterHandle},getAnchorText:function(){return"@"+this.twitterHandle}}),t.match.Url=t.Util.extend(t.match.Match,{constructor:function(e){t.match.Match.prototype.constructor.call(this,e),this.urlMatchType=e.urlMatchType,this.url=e.url,this.protocolUrlMatch=e.protocolUrlMatch,this.protocolRelativeMatch=e.protocolRelativeMatch,this.stripPrefix=e.stripPrefix},urlPrefixRegex:/^(https?:\/\/)?(www\.)?/i,protocolRelativeRegex:/^\/\//,protocolPrepended:!1,getType:function(){return"url"},getUrlMatchType:function(){return this.urlMatchType},getUrl:function(){var t=this.url;return this.protocolRelativeMatch||this.protocolUrlMatch||this.protocolPrepended||(t=this.url="http://"+t,this.protocolPrepended=!0),t},getAnchorHref:function(){var t=this.getUrl();return t.replace(/&amp;/g,"&")},getAnchorText:function(){var t=this.getMatchedText();return this.protocolRelativeMatch&&(t=this.stripProtocolRelativePrefix(t)),this.stripPrefix&&(t=this.stripUrlPrefix(t)),t=this.removeTrailingSlash(t)},stripUrlPrefix:function(t){return t.replace(this.urlPrefixRegex,"")},stripProtocolRelativePrefix:function(t){return t.replace(this.protocolRelativeRegex,"")},removeTrailingSlash:function(t){return"/"===t.charAt(t.length-1)&&(t=t.slice(0,-1)),t}}),t.matcher.Matcher=t.Util.extend(Object,{constructor:function(t){this.tagBuilder=t.tagBuilder},parseMatches:t.Util.abstractMethod}),t.matcher.Email=t.Util.extend(t.matcher.Matcher,{matcherRegex:function(){var e=t.RegexLib.alphaNumericCharsStr,r=new RegExp("["+e+"\\-_';:&=+$.,]+@"),a=t.RegexLib.domainNameRegex,n=t.RegexLib.tldRegex;return new RegExp([r.source,a.source,"\\.",n.source].join(""),"gi")}(),parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.tagBuilder,i=[];null!==(r=a.exec(e));){var s=r[0];i.push(new t.match.Email({tagBuilder:n,matchedText:s,offset:r.index,email:s}))}return i}}),t.matcher.Hashtag=t.Util.extend(t.matcher.Matcher,{matcherRegex:new RegExp("#[_"+t.RegexLib.alphaNumericCharsStr+"]{1,139}","g"),nonWordCharRegex:new RegExp("[^"+t.RegexLib.alphaNumericCharsStr+"]"),constructor:function(e){t.matcher.Matcher.prototype.constructor.call(this,e),this.serviceName=e.serviceName},parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.nonWordCharRegex,i=this.serviceName,s=this.tagBuilder,o=[];null!==(r=a.exec(e));){var c=r.index,h=e.charAt(c-1);if(0===c||n.test(h)){var l=r[0],u=r[0].slice(1);o.push(new t.match.Hashtag({tagBuilder:s,matchedText:l,offset:c,serviceName:i,hashtag:u}))}}return o}}),t.matcher.Phone=t.Util.extend(t.matcher.Matcher,{matcherRegex:/(?:(\+)?\d{1,3}[-\040.])?\(?\d{3}\)?[-\040.]?\d{3}[-\040.]\d{4}/g,parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.tagBuilder,i=[];null!==(r=a.exec(e));){var s=r[0],o=s.replace(/\D/g,""),c=!!r[1];i.push(new t.match.Phone({tagBuilder:n,matchedText:s,offset:r.index,number:o,plusSign:c}))}return i}}),t.matcher.Twitter=t.Util.extend(t.matcher.Matcher,{matcherRegex:new RegExp("@[_"+t.RegexLib.alphaNumericCharsStr+"]{1,20}","g"),nonWordCharRegex:new RegExp("[^"+t.RegexLib.alphaNumericCharsStr+"]"),parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.nonWordCharRegex,i=this.tagBuilder,s=[];null!==(r=a.exec(e));){var o=r.index,c=e.charAt(o-1);if(0===o||n.test(c)){var h=r[0],l=r[0].slice(1);s.push(new t.match.Twitter({tagBuilder:i,matchedText:h,offset:o,twitterHandle:l}))}}return s}}),t.matcher.Url=t.Util.extend(t.matcher.Matcher,{matcherRegex:function(){var e=/(?:[A-Za-z][-.+A-Za-z0-9]*:(?![A-Za-z][-.+A-Za-z0-9]*:\/\/)(?!\d+\/?)(?:\/\/)?)/,r=/(?:www\.)/,a=t.RegexLib.domainNameRegex,n=t.RegexLib.tldRegex,i=t.RegexLib.alphaNumericCharsStr,s=new RegExp("["+i+"\\-+&@#/%=~_()|'$*\\[\\]?!:,.;]*["+i+"\\-+&@#/%=~_()|'$*\\[\\]]");return new RegExp(["(?:","(",e.source,a.source,")","|","(","(//)?",r.source,a.source,")","|","(","(//)?",a.source+"\\.",n.source,")",")","(?:"+s.source+")?"].join(""),"gi")}(),wordCharRegExp:/\w/,openParensRe:/\(/g,closeParensRe:/\)/g,constructor:function(e){t.matcher.Matcher.prototype.constructor.call(this,e),this.stripPrefix=e.stripPrefix},parseMatches:function(e){for(var r,a=this.matcherRegex,n=this.stripPrefix,i=this.tagBuilder,s=[];null!==(r=a.exec(e));){var o=r[0],c=r[1],h=r[2],l=r[3],u=r[5],g=r.index,m=l||u,f=e.charAt(g-1);if(t.matcher.UrlMatchValidator.isValid(o,c)&&!(g>0&&"@"===f||g>0&&m&&this.wordCharRegExp.test(f))){if(this.matchHasUnbalancedClosingParen(o))o=o.substr(0,o.length-1);else{var p=this.matchHasInvalidCharAfterTld(o,c);p>-1&&(o=o.substr(0,p))}var d=c?"scheme":h?"www":"tld",b=!!c;s.push(new t.match.Url({tagBuilder:i,matchedText:o,offset:g,urlMatchType:d,url:o,protocolUrlMatch:b,protocolRelativeMatch:!!m,stripPrefix:n}))}}return s},matchHasUnbalancedClosingParen:function(t){var e=t.charAt(t.length-1);if(")"===e){var r=t.match(this.openParensRe),a=t.match(this.closeParensRe),n=r&&r.length||0,i=a&&a.length||0;if(n<i)return!0}return!1},matchHasInvalidCharAfterTld:function(t,e){if(!t)return-1;var r=0;e&&(r=t.indexOf(":"),t=t.slice(r));var a=/^((.?\/\/)?[A-Za-z0-9\u00C0-\u017F\.\-]*[A-Za-z0-9\u00C0-\u017F\-]\.[A-Za-z]+)/,n=a.exec(t);return null===n?-1:(r+=n[1].length,t=t.slice(n[1].length),/^[^.A-Za-z:\/?#]/.test(t)?r:-1)}}),t.matcher.UrlMatchValidator={hasFullProtocolRegex:/^[A-Za-z][-.+A-Za-z0-9]*:\/\//,uriSchemeRegex:/^[A-Za-z][-.+A-Za-z0-9]*:/,hasWordCharAfterProtocolRegex:/:[^\s]*?[A-Za-z\u00C0-\u017F]/,ipRegex:/[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?\.[0-9][0-9]?[0-9]?/,isValid:function(t,e){return!(e&&!this.isValidUriScheme(e)||this.urlMatchDoesNotHaveProtocolOrDot(t,e)||this.urlMatchDoesNotHaveAtLeastOneWordChar(t,e)&&!this.isValidIpAddress(t))},isValidIpAddress:function(t){var e=new RegExp(this.hasFullProtocolRegex.source+this.ipRegex.source),r=t.match(e);return null!==r},isValidUriScheme:function(t){var e=t.match(this.uriSchemeRegex)[0].toLowerCase();return"javascript:"!==e&&"vbscript:"!==e},urlMatchDoesNotHaveProtocolOrDot:function(t,e){return!(!t||e&&this.hasFullProtocolRegex.test(e)||t.indexOf(".")!==-1)},urlMatchDoesNotHaveAtLeastOneWordChar:function(t,e){return!(!t||!e)&&!this.hasWordCharAfterProtocolRegex.test(t)}},t.truncate.TruncateEnd=function(e,r,a){return t.Util.ellipsis(e,r,a)},t.truncate.TruncateMiddle=function(t,e,r){if(t.length<=e)return t;var a=e-r.length,n="";return a>0&&(n=t.substr(-1*Math.floor(a/2))),(t.substr(0,Math.ceil(a/2))+r+n).substr(0,e)},t.truncate.TruncateSmart=function(t,e,r){var a=function(t){var e={},r=t,a=r.match(/^([a-z]+):\/\//i);return a&&(e.scheme=a[1],r=r.substr(a[0].length)),a=r.match(/^(.*?)(?=(\?|#|\/|$))/i),a&&(e.host=a[1],r=r.substr(a[0].length)),a=r.match(/^\/(.*?)(?=(\?|#|$))/i),a&&(e.path=a[1],r=r.substr(a[0].length)),a=r.match(/^\?(.*?)(?=(#|$))/i),a&&(e.query=a[1],r=r.substr(a[0].length)),a=r.match(/^#(.*?)$/i),a&&(e.fragment=a[1]),e},n=function(t){var e="";return t.scheme&&t.host&&(e+=t.scheme+"://"),t.host&&(e+=t.host),t.path&&(e+="/"+t.path),t.query&&(e+="?"+t.query),t.fragment&&(e+="#"+t.fragment),e},i=function(t,e){var a=e/2,n=Math.ceil(a),i=-1*Math.floor(a),s="";return i<0&&(s=t.substr(i)),t.substr(0,n)+r+s};if(t.length<=e)return t;var s=e-r.length,o=a(t);if(o.query){var c=o.query.match(/^(.*?)(?=(\?|\#))(.*?)$/i);c&&(o.query=o.query.substr(0,c[1].length),t=n(o))}if(t.length<=e)return t;if(o.host&&(o.host=o.host.replace(/^www\./,""),t=n(o)),t.length<=e)return t;var h="";if(o.host&&(h+=o.host),h.length>=s)return o.host.length==e?(o.host.substr(0,e-r.length)+r).substr(0,e):i(h,s).substr(0,e);var l="";if(o.path&&(l+="/"+o.path),o.query&&(l+="?"+o.query),l){if((h+l).length>=s){if((h+l).length==e)return(h+l).substr(0,e);var u=s-h.length;return(h+i(l,u)).substr(0,e)}h+=l}if(o.fragment){var g="#"+o.fragment;if((h+g).length>=s){if((h+g).length==e)return(h+g).substr(0,e);var m=s-h.length;return(h+i(g,m)).substr(0,e)}h+=g}if(o.scheme&&o.host){var f=o.scheme+"://";if((h+f).length<s)return(f+h).substr(0,e)}if(h.length<=e)return h;var p="";return s>0&&(p=h.substr(-1*Math.floor(s/2))),(h.substr(0,Math.ceil(s/2))+r+p).substr(0,e)},t});
diff --git a/packages/rocketchat-channel-settings/client/views/channelSettings.coffee b/packages/rocketchat-channel-settings/client/views/channelSettings.coffee
index 43c310c52f64a754e6d3b5dd7de13b0d4d3a29da..9254eecf1c49441bba8c17c475dab2ebb41d3f4b 100644
--- a/packages/rocketchat-channel-settings/client/views/channelSettings.coffee
+++ b/packages/rocketchat-channel-settings/client/views/channelSettings.coffee
@@ -1,38 +1,35 @@
 Template.channelSettings.helpers
-	canEdit: ->
-		return RocketChat.authz.hasAllPermission('edit-room', @rid)
-	canArchiveOrUnarchive: ->
-		return RocketChat.authz.hasAtLeastOnePermission(['archive-room', 'unarchive-room'], @rid)
+	toArray: (obj) ->
+		arr = []
+		for key, value of obj
+			arr.push
+				$key: key
+				$value: value
+		return arr
+
+	valueOf: (obj, key) ->
+		return obj?[key]
+
+	showSetting: (setting, room) ->
+		if setting.showInDirect is false
+			return room.t isnt 'd'
+		return true
+
+	settings: ->
+		return Template.instance().settings
+
+	getRoom: ->
+		return ChatRoom.findOne(@rid)
+
 	editing: (field) ->
 		return Template.instance().editing.get() is field
-	notDirect: ->
-		return ChatRoom.findOne(@rid, { fields: { t: 1 }})?.t isnt 'd'
-	roomType: ->
-		return ChatRoom.findOne(@rid, { fields: { t: 1 }})?.t
+
 	channelSettings: ->
 		return RocketChat.ChannelSettings.getOptions()
-	roomTypeDescription: ->
-		roomType = ChatRoom.findOne(@rid, { fields: { t: 1 }})?.t
-		if roomType is 'c'
-			return t('Channel')
-		else if roomType is 'p'
-			return t('Private_Group')
-	roomName: ->
-		return ChatRoom.findOne(@rid, { fields: { name: 1 }})?.name
-	roomTopic: ->
-		return ChatRoom.findOne(@rid, { fields: { topic: 1 }})?.topic
-	roomTopicUnescaped: ->
-		return s.unescapeHTML ChatRoom.findOne(@rid, { fields: { topic: 1 }})?.topic
-	roomDescription: ->
-		return ChatRoom.findOne(@rid, { fields: { description: 1 }})?.description
-	archivationState: ->
-		return ChatRoom.findOne(@rid, { fields: { archived: 1 }})?.archived
-	archivationStateDescription: ->
-		archivationState = ChatRoom.findOne(@rid, { fields: { archived: 1 }})?.archived
-		if archivationState is true
-			return t('Room_archivation_state_true')
-		else
-			return t('Room_archivation_state_false')
+
+	unscape: (value) ->
+		return s.unescapeHTML value
+
 	canDeleteRoom: ->
 		roomType = ChatRoom.findOne(@rid, { fields: { t: 1 }})?.t
 		return roomType? and RocketChat.authz.hasAtLeastOnePermission("delete-#{roomType}", @rid)
@@ -95,85 +92,120 @@ Template.channelSettings.events
 Template.channelSettings.onCreated ->
 	@editing = new ReactiveVar
 
-	@validateRoomType = =>
-		type = @$('input[name=roomType]:checked').val()
-		if type not in ['c', 'p']
-			toastr.error t('error-invalid-room-type', type)
-		return true
-
-	@validateRoomName = =>
-		rid = Template.currentData()?.rid
-		room = ChatRoom.findOne rid
-
-		if not RocketChat.authz.hasAllPermission('edit-room', rid) or room.t not in ['c', 'p']
-			toastr.error t('error-not-allowed')
-			return false
-
-		name = $('input[name=roomName]').val()
+	@settings =
+		name:
+			type: 'text'
+			label: 'Name'
+			canView: (room) => room.t isnt 'd'
+			canEdit: (room) => RocketChat.authz.hasAllPermission('edit-room', room._id)
+			save: (value, room) ->
+				if not RocketChat.authz.hasAllPermission('edit-room', room._id) or room.t not in ['c', 'p']
+					return toastr.error t('error-not-allowed')
+
+				try
+					nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$'
+				catch
+					nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$'
+
+				if not nameValidation.test value
+					return toastr.error t('error-invalid-room-name', { room_name: name: value })
+
+				if @validateRoomName()
+					RocketChat.callbacks.run 'roomNameChanged', { _id: room._id, name: value }
+					Meteor.call 'saveRoomSettings', room._id, 'roomName', value, (err, result) ->
+						return handleError err if err
+						toastr.success TAPi18n.__ 'Room_name_changed_successfully'
+
+		topic:
+			type: 'markdown'
+			label: 'Topic'
+			canView: (room) => true
+			canEdit: (room) => RocketChat.authz.hasAllPermission('edit-room', room._id)
+			save: (value, room) ->
+				Meteor.call 'saveRoomSettings', room._id, 'roomTopic', value, (err, result) ->
+					return handleError err if err
+					toastr.success TAPi18n.__ 'Room_topic_changed_successfully'
+					RocketChat.callbacks.run 'roomTopicChanged', room
+
+		description:
+			type: 'text'
+			label: 'Description'
+			canView: (room) => room.t isnt 'd'
+			canEdit: (room) => RocketChat.authz.hasAllPermission('edit-room', room._id)
+			save: (value, room) ->
+				Meteor.call 'saveRoomSettings', room._id, 'roomDescription', value, (err, result) ->
+					return handleError err if err
+					toastr.success TAPi18n.__ 'Room_description_changed_successfully'
+
+		t:
+			type: 'select'
+			label: 'Type'
+			options:
+				c: 'Channel'
+				p: 'Private_Group'
+			canView: (room) => room.t in ['c', 'p']
+			canEdit: (room) => RocketChat.authz.hasAllPermission('edit-room', room._id)
+			save: (value, room) ->
+				console.log value
+				if value not in ['c', 'p']
+					return toastr.error t('error-invalid-room-type', value)
+
+				RocketChat.callbacks.run 'roomTypeChanged', room
+				Meteor.call 'saveRoomSettings', room._id, 'roomType', value, (err, result) ->
+					return handleError err if err
+					toastr.success TAPi18n.__ 'Room_type_changed_successfully'
+
+		ro:
+			type: 'boolean'
+			label: 'Read_only'
+			canView: (room) => room.t isnt 'd'
+			canEdit: (room) => RocketChat.authz.hasAllPermission('set-readonly', room._id)
+			save: (value, room) ->
+				Meteor.call 'saveRoomSettings', room._id, 'readOnly', value, (err, result) ->
+					return handleError err if err
+					toastr.success TAPi18n.__ 'Read_only_changed_successfully'					
+
+		archived:
+			type: 'boolean'
+			label: 'Room_archivation_state_true'
+			canView: (room) => room.t isnt 'd'
+			canEdit: (room) => RocketChat.authz.hasAtLeastOnePermission(['archive-room', 'unarchive-room'], room._id)
+			save: (value, room) ->
+				if value is true
+					Meteor.call 'archiveRoom', room._id, (err, results) ->
+						return handleError err if err
+						toastr.success TAPi18n.__ 'Room_archived'
+						RocketChat.callbacks.run 'archiveRoom', room
+				else
+					Meteor.call 'unarchiveRoom', room._id, (err, results) ->
+						return handleError err if err
+						toastr.success TAPi18n.__ 'Room_unarchived'
+						RocketChat.callbacks.run 'unarchiveRoom', room
+
+		joinCode:
+			type: 'text'
+			label: 'Code'
+			canView: (room) => room.t is 'c' and RocketChat.authz.hasAllPermission('edit-room', room._id)
+			canEdit: (room) => RocketChat.authz.hasAllPermission('edit-room', room._id)
+			save: (value, room) ->
+				Meteor.call 'saveRoomSettings', room._id, 'joinCode', value, (err, result) ->
+					return handleError err if err
+					toastr.success TAPi18n.__ 'Room_code_changed_successfully'
+					RocketChat.callbacks.run 'roomCodeChanged', room
 
-		try
-			nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$'
-		catch
-			nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$'
 
-		if not nameValidation.test name
-			toastr.error t('error-invalid-room-name', { room_name: name: name })
-			return false
+	@saveSetting = =>
+		room = ChatRoom.findOne @data?.rid
+		field = @editing.get()
 
-		return true
+		if @settings[field].type is 'select'
+			value = @$(".channel-settings form [name=#{field}]:checked").val()
+		else if @settings[field].type is 'boolean'
+			value = @$(".channel-settings form [name=#{field}]:checked").val() is 'true'
+		else
+			value = @$(".channel-settings form [name=#{field}]").val()
 
-	@validateRoomTopic = =>
-		return true
+		if value isnt room[field]
+			@settings[field].save(value, room)
 
-	@saveSetting = =>
-		room = ChatRoom.findOne @data?.rid
-		switch @editing.get()
-			when 'roomName'
-				if $('input[name=roomName]').val() is room.name
-					toastr.success TAPi18n.__ 'Room_name_changed_successfully'
-					RocketChat.callbacks.run 'roomNameChanged', ChatRoom.findOne(room._id)
-				else
-					if @validateRoomName()
-						RocketChat.callbacks.run 'roomNameChanged', { _id: room._id, name: @$('input[name=roomName]').val() }
-						Meteor.call 'saveRoomSettings', room._id, 'roomName', @$('input[name=roomName]').val(), (err, result) ->
-							return handleError err if err
-							toastr.success TAPi18n.__ 'Room_name_changed_successfully'
-			when 'roomTopic'
-				if @validateRoomTopic()
-					Meteor.call 'saveRoomSettings', room._id, 'roomTopic', @$('input[name=roomTopic]').val(), (err, result) ->
-						return handleError err if err
-						toastr.success TAPi18n.__ 'Room_topic_changed_successfully'
-						RocketChat.callbacks.run 'roomTopicChanged', ChatRoom.findOne(result.rid)
-			when 'roomType'
-				if @validateRoomType()
-					RocketChat.callbacks.run 'roomTypeChanged', room
-					Meteor.call 'saveRoomSettings', room._id, 'roomType', @$('input[name=roomType]:checked').val(), (err, result) ->
-						return handleError err if err
-						toastr.success TAPi18n.__ 'Room_type_changed_successfully'
-			when 'roomDescription'
-				if @validateRoomTopic()
-					Meteor.call 'saveRoomSettings', room._id, 'roomDescription', @$('input[name=roomDescription]').val(), (err, result) ->
-						return handleError err if err
-						toastr.success TAPi18n.__ 'Room_description_changed_successfully'
-			when 'archivationState'
-				if @$('input[name=archivationState]:checked').val() is 'true'
-					if room.archived isnt true
-						Meteor.call 'archiveRoom', room._id, (err, results) ->
-							return handleError err if err
-							toastr.success TAPi18n.__ 'Room_archived'
-							RocketChat.callbacks.run 'archiveRoom', ChatRoom.findOne(room._id)
-				else
-					if room.archived is true
-						Meteor.call 'unarchiveRoom', room._id, (err, results) ->
-							return handleError err if err
-							toastr.success TAPi18n.__ 'Room_unarchived'
-							RocketChat.callbacks.run 'unarchiveRoom', ChatRoom.findOne(room._id)
-			when 'readOnly'
-				Meteor.call 'saveRoomSettings', room._id, 'readOnly', @$('input[name=readOnly]:checked').val() is 'true', (err, result) ->
-					return handleError err if err
-					toastr.success TAPi18n.__ 'Read_only_changed_successfully'
-			when 'systemMessages'
-				Meteor.call 'saveRoomSettings', room._id, 'systemMessages', @$('input[name=systemMessages]:checked').val() is 'true', (err, result) ->
-					return handleError err if err
-					toastr.success TAPi18n.__ 'System_messages_setting_changed_successfully'
 		@editing.set()
diff --git a/packages/rocketchat-channel-settings/client/views/channelSettings.html b/packages/rocketchat-channel-settings/client/views/channelSettings.html
index ed32761314fdc8f04ca0e9320bba5805b8034bb1..c1d0c15fd792d46665784f778b5e3dc4655f631b 100644
--- a/packages/rocketchat-channel-settings/client/views/channelSettings.html
+++ b/packages/rocketchat-channel-settings/client/views/channelSettings.html
@@ -6,92 +6,75 @@
 			</div>
 			<form>
 				<ul class="list clearfix">
-					{{#if notDirect}}
-						<li>
-							<label>{{_ "Name"}}</label>
-							<div>
-								{{#if editing 'roomName'}}
-									<input type="text" name="roomName" value="{{roomName}}" class="editing" /> <button type="button" class="button secondary cancel">{{_ "Cancel"}}</button> <button type="button" class="button primary save">{{_ "Save"}}</button>
-								{{else}}
-									<span>{{roomName}}{{#if canEdit}} <i class="icon-pencil" data-edit="roomName"></i>{{/if}}</span>
-								{{/if}}
-							</div>
-						</li>
-					{{/if}}
-					<li>
-						<label>{{_ "Topic"}}</label>
-						<div>
-							{{#if editing 'roomTopic'}}
-								<input type="text" name="roomTopic" value="{{roomTopicUnescaped}}" class="editing" /> <button type="button" class="button secondary cancel">{{_ "Cancel"}}</button> <button type="button" class="button primary save">{{_ "Save"}}</button>
-							{{else}}
-								<span>{{{RocketChatMarkdown roomTopic}}}{{#if canEdit}} <i class="icon-pencil" data-edit="roomTopic"></i>{{/if}}</span>
+					{{#let room=getRoom}}
+						{{#each toArray settings}}
+
+							{{#if $value.canView room}}
+								{{#let value=(valueOf room $key)}}
+									<li>
+										<label>{{_ $value.label}}</label>
+										<div>
+											{{#if $eq $value.type 'text'}}
+												{{#if editing $key}}
+													<input type="text" name="{{$key}}" value="{{value}}" class="editing" />
+												{{else}}
+													<span>{{value}}</span>
+												{{/if}}
+											{{/if}}
+
+											{{#if $eq $value.type 'markdown'}}
+												{{#if editing $key}}
+													<input type="text" name="{{$key}}" value="{{unscape value}}" class="editing" />
+												{{else}}
+													<span>{{{RocketChatMarkdown value}}}</span>
+												{{/if}}
+											{{/if}}
+
+											{{#if $eq $value.type 'select'}}
+												{{#if editing $key}}
+													{{#each toArray $value.options}}
+														<label>
+															<input type="radio" name="{{../$key}}" value="{{$key}}" checked="{{$eq value $key}}" class="editing" />
+															{{_ $value}}
+														</label>
+													{{/each}}
+												{{else}}
+													<span>{{_ (valueOf $value.options value)}}</span>
+												{{/if}}
+											{{/if}}
+
+											{{#if $eq $value.type 'boolean'}}
+												{{#if editing $key}}
+													<label>
+														<input type="radio" name="{{$key}}" value="true" checked="{{$eq value true}}" class="editing" />
+														{{_ 'True'}}
+													</label>
+													<label>
+														<input type="radio" name="{{$key}}" value="false" checked="{{$neq value true}}" class="editing" />
+														{{_ 'False'}}
+													</label>
+												{{else}}
+													{{#if value}}
+														<span>{{_ 'True'}}</span>
+													{{else}}
+														<span>{{_ 'False'}}</span>
+													{{/if}}
+												{{/if}}
+											{{/if}}
+
+											{{#if editing $key}}
+												<button type="button" class="button secondary cancel">{{_ "Cancel"}}</button>
+												<button type="button" class="button primary save">{{_ "Save"}}</button>
+											{{else}}
+												<span>{{#if $value.canEdit room}} <i class="icon-pencil" data-edit="{{$key}}"></i>{{/if}}</span>
+											{{/if}}
+										</div>
+									</li>
+								{{/let}}
 							{{/if}}
-						</div>
-					</li>
-					{{#if notDirect}}
-						<li>
-							<label>{{_ "Description"}}</label>
-						<div>
-							{{#if editing 'roomDescription'}}
-								<input type="text" name="roomDescription" value="{{roomDescription}}" class="editing" /> <button type="button" class="button secondary cancel">{{_ "Cancel"}}</button><button type="button" class="button primary save">{{_ "Save"}}</button>
-							{{else}}
-								<span>{{roomDescription}}{{#if canEdit}} <i class="icon-pencil" data-edit="roomDescription"></i>{{/if}}</span>
-							{{/if}}
-							</div>
-						</li>
-						<li>
-							<label>{{_ "Type"}}</label>
-							<div>
-								{{#if editing 'roomType'}}
-									<label><input type="radio" name="roomType" class="editing" value="c" checked="{{$eq roomType 'c'}}" /> {{_ "Channel"}}</label>
-									<label><input type="radio" name="roomType" value="p" checked="{{$eq roomType 'p'}}" /> {{_ "Private_Group"}}</label>
-									<button type="button" class="button secondary cancel">{{_ "Cancel"}}</button>
-									<button type="button" class="button primary save">{{_ "Save"}}</button>
-								{{else}}
-									<span>{{roomTypeDescription}}{{#if canEdit}} <i class="icon-pencil" data-edit="roomType"></i>{{/if}}</span>
-								{{/if}}
-							</div>
-						</li>
-						<li>
-							<label>{{_ "Room_archivation_state"}}</label>
-							<div>
-								{{#if editing 'archivationState'}}
-									<label><input type="radio" name="archivationState" class="editing" value="true" checked="{{$eq archivationState true}}" /> {{_ "Room_archivation_state_true"}}</label>
-									<label><input type="radio" name="archivationState" value="false" checked="{{$neq archivationState true}}" /> {{_ "Room_archivation_state_false"}}</label>
-									<button type="button" class="button secondary cancel">{{_ "Cancel"}}</button>
-									<button type="button" class="button primary save">{{_ "Save"}}</button>
-								{{else}}
-									<span>{{archivationStateDescription}}{{#if canArchiveOrUnarchive}} <i class="icon-pencil" data-edit="archivationState"></i>{{/if}}</span>
-								{{/if}}
-							</div>
-						</li>
-						<li>
-							<label>{{_ "Read_only"}}</label>
-							<div>
-								{{#if editing 'readOnly'}}
-									<label><input type="radio" name="readOnly" class="editing" value="true" checked="{{$eq readOnly true}}" /> {{_ "True"}}</label>
-									<label><input type="radio" name="readOnly" value="false" checked="{{$neq readOnly true}}" /> {{_ "False"}}</label>
-									<button type="button" class="button secondary cancel">{{_ "Cancel"}}</button>
-									<button type="button" class="button primary save">{{_ "Save"}}</button>
-								{{else}}
-									<span>{{#if readOnly}}{{_ "True"}}{{else}}{{_ "False"}}{{/if}}{{#if canEdit}} <i class="icon-pencil" data-edit="readOnly"></i>{{/if}}</span>
-								{{/if}}
-							</div>
-						</li>
-						<li>
-							<label>{{_ "System_messages"}}</label>
-							<div>
-								{{#if editing 'systemMessages'}}
-									<label><input type="radio" name="systemMessages" class="editing" value="true" checked="{{$eq systemMessages true}}" /> {{_ "On"}}</label>
-									<label><input type="radio" name="systemMessages" value="false" checked="{{$neq systemMessages true}}" /> {{_ "Off"}}</label>
-									<button type="button" class="button secondary cancel">{{_ "Cancel"}}</button>
-									<button type="button" class="button primary save">{{_ "Save"}}</button>
-								{{else}}
-									<span>{{#if systemMessages}}{{_ "On"}}{{else}}{{_ "Off"}}{{/if}}{{#if canEdit}} <i class="icon-pencil" data-edit="systemMessages"></i>{{/if}}</span>
-								{{/if}}
-							</div>
-						</li>
-					{{/if}}
+						{{/each}}
+					{{/let}}
+
 					{{#each channelSettings}}
 						{{> Template.dynamic template=template data=data}}
 					{{/each}}
diff --git a/packages/rocketchat-channel-settings/server/functions/saveRoomName.coffee b/packages/rocketchat-channel-settings/server/functions/saveRoomName.coffee
index 5f00ae48193431fcde32c9c739659ec4317e8783..929b05fa87120c0d811c83536a815714479fabc8 100644
--- a/packages/rocketchat-channel-settings/server/functions/saveRoomName.coffee
+++ b/packages/rocketchat-channel-settings/server/functions/saveRoomName.coffee
@@ -1,15 +1,9 @@
-RocketChat.saveRoomName = (rid, name) ->
-	if not Meteor.userId()
-		throw new Meteor.Error('error-invalid-user', "Invalid user", { function: 'RocketChat.saveRoomName' })
-
+RocketChat.saveRoomName = (rid, name, user) ->
 	room = RocketChat.models.Rooms.findOneById rid
 
 	if room.t not in ['c', 'p']
 		throw new Meteor.Error 'error-not-allowed', 'Not allowed', { function: 'RocketChat.saveRoomName' }
 
-	unless RocketChat.authz.hasPermission(Meteor.userId(), 'edit-room', rid)
-		throw new Meteor.Error 'error-not-allowed', 'Not allowed', { function: 'RocketChat.saveRoomName' }
-
 	try
 		nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$'
 	catch
diff --git a/packages/rocketchat-channel-settings/server/functions/saveRoomTopic.coffee b/packages/rocketchat-channel-settings/server/functions/saveRoomTopic.coffee
index 3ffc8b54f1cac85aa704d0fbdc98f26bd68e3b6b..216eb7a2d10fe36b805c29c83725b3b1bbe208a3 100644
--- a/packages/rocketchat-channel-settings/server/functions/saveRoomTopic.coffee
+++ b/packages/rocketchat-channel-settings/server/functions/saveRoomTopic.coffee
@@ -6,6 +6,4 @@ RocketChat.saveRoomTopic = (rid, roomTopic, user) ->
 
 	update = RocketChat.models.Rooms.setTopicById(rid, roomTopic)
 
-	RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser 'room_changed_topic', rid, roomTopic, user
-
 	return update
diff --git a/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.coffee b/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.coffee
index 47da17997fe294c19eaa5a682168bc956d2ff65c..f9b394f3d80cb172b8231326718dc9a1a674a5ee 100644
--- a/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.coffee
+++ b/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.coffee
@@ -1,9 +1,12 @@
 Meteor.methods
 	saveRoomSettings: (rid, setting, value) ->
+		if not Meteor.userId()
+			throw new Meteor.Error('error-invalid-user', "Invalid user", { function: 'RocketChat.saveRoomName' })
+
 		unless Match.test rid, String
 			throw new Meteor.Error 'error-invalid-room', 'Invalid room', { method: 'saveRoomSettings' }
 
-		if setting not in ['roomName', 'roomTopic', 'roomDescription', 'roomType', 'readOnly', 'systemMessages', 'default']
+		if setting not in ['roomName', 'roomTopic', 'roomDescription', 'roomType', 'readOnly', 'systemMessages', 'default', 'joincode']
 			throw new Meteor.Error 'error-invalid-settings', 'Invalid settings provided', { method: 'saveRoomSettings' }
 
 		unless RocketChat.authz.hasPermission(Meteor.userId(), 'edit-room', rid)
@@ -21,6 +24,7 @@ Meteor.methods
 				when 'roomTopic'
 					if value isnt room.topic
 						RocketChat.saveRoomTopic(rid, value, Meteor.user())
+						RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser 'room_changed_topic', rid, value, Meteor.user()
 				when 'roomDescription'
 					if value isnt room.description
 						RocketChat.saveRoomDescription rid, value, Meteor.user()
@@ -38,6 +42,8 @@ Meteor.methods
 				when 'systemMessages'
 					if value isnt room.sysMes
 						RocketChat.saveRoomSystemMessages rid, value, Meteor.user()
+				when 'joinCode'
+					RocketChat.models.Rooms.setJoinCodeById rid, String(value)
 				when 'default'
 					RocketChat.models.Rooms.saveDefaultById rid, value
 
diff --git a/packages/rocketchat-custom-oauth/custom_oauth_server.coffee b/packages/rocketchat-custom-oauth/custom_oauth_server.coffee
index baa17cb6a6dcb18bb21f06528c49d9b886a508b0..5bb71cb06452990e6fbc3d58b397be3dae882fc9 100644
--- a/packages/rocketchat-custom-oauth/custom_oauth_server.coffee
+++ b/packages/rocketchat-custom-oauth/custom_oauth_server.coffee
@@ -122,16 +122,20 @@ class CustomOAuth
 
 			if identity?.CharacterID and not identity.id
 				identity.id = identity.CharacterID
-			
+
 			# Fix Dataporten having 'user.userid' instead of 'id'
 			if identity?.user?.userid and not identity.id
 				identity.id = identity.user.userid
 				identity.email = identity.user.email
-			
+
+			# Fix general 'phid' instead of 'id' from phabricator
+			if identity?.phid and not identity.id
+				identity.id = identity.phid
+
 			# Fix general 'userid' instead of 'id' from provider
 			if identity?.userid and not identity.id
 				identity.id = identity.userid
-				
+
 			# console.log 'id:', JSON.stringify identity, null, '  '
 
 			serviceData =
@@ -144,7 +148,7 @@ class CustomOAuth
 				serviceData: serviceData
 				options:
 					profile:
-						name: identity.name or identity.username or identity.nickname or identity.CharacterName or identity.user?.name
+						name: identity.name or identity.username or identity.nickname or identity.CharacterName or identity.userName or identity.user?.name
 
 			# console.log data
 
diff --git a/packages/rocketchat-gitlab/common.coffee b/packages/rocketchat-gitlab/common.coffee
index 7d6cd560398f99966159fbe4c053b6d661349826..42bdd4ac82dfe1e6e7792bd4595d0741e51dd9e1 100644
--- a/packages/rocketchat-gitlab/common.coffee
+++ b/packages/rocketchat-gitlab/common.coffee
@@ -1,6 +1,7 @@
 config =
 	serverURL: 'https://gitlab.com'
 	identityPath: '/api/v3/user'
+	scope: 'api'
 	addAutopublishFields:
 		forLoggedInUser: ['services.gitlab']
 		forOtherUsers: ['services.gitlab.username']
diff --git a/packages/rocketchat-importer-hipchat/server.coffee b/packages/rocketchat-importer-hipchat/server.coffee
index 3644f0bd04fa95ba048c6083cea2db711f7b633a..137f1de40b82eeb89f2c624e4bcd42b55859931f 100644
--- a/packages/rocketchat-importer-hipchat/server.coffee
+++ b/packages/rocketchat-importer-hipchat/server.coffee
@@ -135,7 +135,7 @@ Importer.HipChat = class Importer.HipChat extends Importer.Base
 								Meteor.call 'setUsername', user.mention_name
 								Meteor.call 'joinDefaultChannels', true
 								Meteor.call 'setAvatarFromService', user.photo_url, null, 'url'
-								Meteor.call 'updateUserUtcOffset', parseInt moment().tz(user.timezone).format('Z').toString().split(':')[0]
+								Meteor.call 'userSetUtcOffset', parseInt moment().tz(user.timezone).format('Z').toString().split(':')[0]
 
 							if user.name?
 								RocketChat.models.Users.setName userId, user.name
diff --git a/packages/rocketchat-importer-slack/server.coffee b/packages/rocketchat-importer-slack/server.coffee
index 242533f824dfb21884c4b39487b04f64691c51ee..1dde233446e9a047b98841daa95981b2bdb11294 100644
--- a/packages/rocketchat-importer-slack/server.coffee
+++ b/packages/rocketchat-importer-slack/server.coffee
@@ -135,7 +135,7 @@ Importer.Slack = class Importer.Slack extends Importer.Base
 								Meteor.call 'setAvatarFromService', url, null, 'url'
 								# Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600
 								if user.tz_offset
-									Meteor.call 'updateUserUtcOffset', user.tz_offset / 3600
+									Meteor.call 'userSetUtcOffset', user.tz_offset / 3600
 
 							RocketChat.models.Users.update { _id: userId }, { $addToSet: { importIds: user.id } }
 
diff --git a/packages/rocketchat-ldap/server/loginHandler.js b/packages/rocketchat-ldap/server/loginHandler.js
index 13342ab8ddbdf952597b7494fe56ace6880f02e5..53bc272ca872ccb98ce239784392bb743d8a5681 100644
--- a/packages/rocketchat-ldap/server/loginHandler.js
+++ b/packages/rocketchat-ldap/server/loginHandler.js
@@ -60,7 +60,7 @@ Accounts.registerLoginHandler('ldap', function(loginRequest) {
 
 	ldap.disconnect();
 
-	if (ldapUser === undefined) {
+	if (ldapUser === undefined && RocketChat.settings.get('LDAP_Login_Fallback') === true) {
 		return fallbackDefaultAccountSystem(self, loginRequest.username, loginRequest.ldapPass);
 	}
 
diff --git a/packages/rocketchat-ldap/server/settings.js b/packages/rocketchat-ldap/server/settings.js
index df702f515b21be8a604a52deb5f50ab5778a8979..544983b6bf5d5d73dd8d45c9ddca3e49341a3c3d 100644
--- a/packages/rocketchat-ldap/server/settings.js
+++ b/packages/rocketchat-ldap/server/settings.js
@@ -19,6 +19,7 @@ Meteor.startup(function() {
 		];
 
 		this.add('LDAP_Enable', false, { type: 'boolean', public: true });
+		this.add('LDAP_Login_Fallback', true, { type: 'boolean', enableQuery: enableQuery });
 		this.add('LDAP_Host', '', { type: 'string', enableQuery: enableQuery });
 		this.add('LDAP_Port', '389', { type: 'string', enableQuery: enableQuery });
 		this.add('LDAP_Encryption', 'plain', { type: 'select', values: [ { key: 'plain', i18nLabel: 'No_Encryption' }, { key: 'tls', i18nLabel: 'StartTLS' }, { key: 'ssl', i18nLabel: 'SSL/LDAPS' } ], enableQuery: enableQuery });
diff --git a/packages/rocketchat-lib/i18n/ca.i18n.json b/packages/rocketchat-lib/i18n/ca.i18n.json
index 3ba9716c3630b837ffd8a3db8ff5d459acf2314e..bfd9d6c1e4eee828ba29ce2e729dbabd05660744 100644
--- a/packages/rocketchat-lib/i18n/ca.i18n.json
+++ b/packages/rocketchat-lib/i18n/ca.i18n.json
@@ -29,6 +29,8 @@
   "Accounts_BlockedDomainsList_Description" : "Llista de dominis bloquejats separada per comes",
   "Accounts_BlockedUsernameList" : "Llista de noms d'usuari bloquejats",
   "Accounts_BlockedUsernameList_Description" : "Llista  separada per comes de noms d'usuari bloquejats (no distingeix majúscules/minúscules)",
+  "Accounts_CustomFields" : "Traduccions personalitzades",
+  "Accounts_CustomFields_Description" : "Ha de ser un objecte JSON vàlid on les claus són els noms dels camps i contenen un diccionari amb les opcions del camp. Exemple:</br><code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"student\",\n  \"options\": [\"teacher\", \"student\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code> ",
   "Accounts_denyUnverifiedEmail" : "Denegar correu electrònic sense verificar",
   "Accounts_EmailVerification" : "Verificació de correu electrònic",
   "Accounts_EmailVerification_Description" : "Assegura't que la configuració SMTP és correcta per fer servir aquesta funcionalitat",
@@ -100,6 +102,7 @@
   "Accounts_RegistrationForm_SecretURL" : "URL secret del fomulari de registre",
   "Accounts_RegistrationForm_SecretURL_Description" : "Cal proporcionar una cadena de text aleatori que s'afegirà a l'URL de registre. Exemple: https://demo.rocket.chat/register/[secret_hash]",
   "Accounts_RequireNameForSignUp" : "Requerir el nom per registrar-se",
+  "Accounts_RequirePasswordConfirmation" : "Requereix confirmació de la contrasenya",
   "Accounts_ShowFormLogin" : "Mostra inici de sessió basat en formulari",
   "Accounts_UseDefaultBlockedDomainsList" : "Utilitza la llista predeterminada de dominis bloquejats",
   "Accounts_UseDNSDomainCheck" : "Utilitza la comprovació DNS de dominis",
@@ -276,6 +279,8 @@
   "Custom_oauth_unique_name" : "Nom únic de OAuth personalitzat",
   "Custom_Script_Logged_In" : "Script personalitzat per als usuaris en identificar-se",
   "Custom_Script_Logged_Out" : "Script personalitzat per als usuaris en sortir",
+  "Custom_Translations" : "Traduccions personalitzades",
+  "Custom_Translations_Description" : "Ha de ser un objecte JSON vàlid on les claus són el codi de l'idioma i contenen un diccionari de clau (key) i traduccions. Exemple:</br><code>{\n \"en\": {\n  \"key\": \"translation\"\n },\n \"ca\": {\n  \"key\": \"traducció\"\n }\n}</code> ",
   "Dashboard" : "Tauler",
   "Date" : "Data",
   "days" : "dies",
@@ -427,6 +432,7 @@
   "Features_Enabled" : "Funcionalitats habilitades",
   "Field" : "Camp",
   "Field_removed" : "Camp eliminat",
+  "Field_required" : "Camp obligatori",
   "File_exceeds_allowed_size_of_bytes" : "L'arxiu supera la mida màxima de __size__ bytes",
   "FileUpload" : "Pujar arxius",
   "FileUpload_Enabled" : "Habilita pujar arxius",
@@ -626,6 +632,8 @@
   "LDAP_Encryption_Description" : "Mètode de xifrat utilitzat per a la comunicació segura cap al servidor LDAP. Alguns exemples 'sense xifrat', 'SSL / LDAPS (xifrat des de l'inici), i' StartTLS '(actualitzar a comunicacions xifrades una vegada connectat).",
   "LDAP_Host" : "Amfitrió (host) ",
   "LDAP_Host_Description" : "Amfitrió (host) LDAP, exemple: `ldap.exemple.com` o `10.0.0.30`.",
+  "LDAP_Import_Users" : "Importa usuaris LDAP",
+  "LDAP_Import_Users_Description" : "Si s'activa, el procés de sincronització importarà tots els usuaris LDAP<br/> *Compte!* Cal especificar un filtre de cerca per no importar més usuaris del compte.",
   "LDAP_Merge_Existing_Users" : "Uneix usuaris existents",
   "LDAP_Merge_Existing_Users_Description" : "*Atenció!* Quan s'importa un usuari LDAP i ja existeix un usuari amb el mateix nom d'usuari, es canviarà la informació LDAP i el password de l'usuari ja existent.",
   "LDAP_Port" : "Port",
@@ -706,6 +714,7 @@
   "Markdown_Headers" : "Encapçalaments Markdown",
   "Markdown_SupportSchemesForLink" : "Markdown detecta scheme:// com a enllaç",
   "Markdown_SupportSchemesForLink_Description" : "Llista dels scheme:// permesos separats per comes",
+  "Max_length_is" : "La llargada màxima és %s",
   "Members_List" : "Llista de membres",
   "Mentions" : "Mencions",
   "Mentions_default" : "Mencions (per defecte)",
@@ -756,6 +765,7 @@
   "Meta_language" : "Idioma",
   "Meta_msvalidate01" : "MSValidate.01",
   "Meta_robots" : "Robots",
+  "Min_length_is" : "La llargada mínima és %s",
   "minutes" : "minuts",
   "Mobile_push" : "Push mòbil ",
   "Monitor_history_for_changes_on" : "Monitoritza l'historial per canvis a ",
@@ -1307,6 +1317,7 @@
   "You_have_been_muted" : "Has estat silenciat i no podràs dir res en aquesta sala",
   "You_have_not_verified_your_email" : "Encara no has verificat la teva adreça de correu electrònic.",
   "You_have_successfully_unsubscribed" : "T'has donat de baixa correctament de la nostra llista de distribució de correu.",
+  "You_must_join_to_view_messages_in_this_channel" : "Has d'unir-te per veure els missatges d'aquest canal",
   "You_need_confirm_email" : "Cal que confirmis la teva adreça de correu-e per poder identificar-te.",
   "You_need_install_an_extension_to_allow_screen_sharing" : "Necessita instal·lar una extensió per poder compartir la pantalla",
   "You_need_to_change_your_password" : "Cal canvïs la contrasenya",
diff --git a/packages/rocketchat-lib/i18n/cs.i18n.json b/packages/rocketchat-lib/i18n/cs.i18n.json
index be5caed885225c64b494d15454cbf049a101a942..ac194c461ad71feca6e9d4f228fd34a6b55b8f6f 100644
--- a/packages/rocketchat-lib/i18n/cs.i18n.json
+++ b/packages/rocketchat-lib/i18n/cs.i18n.json
@@ -29,6 +29,8 @@
   "Accounts_BlockedDomainsList_Description" : "Čárkami oddělený seznam blokovaných domén",
   "Accounts_BlockedUsernameList" : "Zakázaná uživatelská jména",
   "Accounts_BlockedUsernameList_Description" : "čárkou oddělený seznam uživatelských jmen (na velikosti písmen nezáleží)",
+  "Accounts_CustomFields" : "Vlastní překlad",
+  "Accounts_CustomFields_Description" : "Validní JSON obsahující klíče polí s nastavením. Například:<br/> <code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"student\",\n  \"options\": [\"teacher\", \"student\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code>",
   "Accounts_denyUnverifiedEmail" : "Zakázat neověřené e-mailové adresy",
   "Accounts_EmailVerification" : "Ověření e-mailu",
   "Accounts_EmailVerification_Description" : "Pro použití této funkce se ujistěte, že máte správné nastavení SMTP",
@@ -100,6 +102,7 @@
   "Accounts_RegistrationForm_SecretURL" : "Tajná URL registrace",
   "Accounts_RegistrationForm_SecretURL_Description" : "Vložte náhodný retězec, který bude přidán do vaší registrační URL. Příklad: https://demo.rocket.chat/register/[tajny_kod]",
   "Accounts_RequireNameForSignUp" : "Vyžadovat jméno",
+  "Accounts_RequirePasswordConfirmation" : "Vyžadovat potvrzení hesla",
   "Accounts_ShowFormLogin" : "Zobrazit formulářové přihlášení",
   "Accounts_UseDefaultBlockedDomainsList" : "Použít výchozí seznam blokovaných domén",
   "Accounts_UseDNSDomainCheck" : "Použít ověření DNS domény",
@@ -170,6 +173,7 @@
   "Are_you_sure_you_want_to_delete_your_account" : "Jste si jisti, že chcete smazat svůj účet?",
   "Assign_admin" : "Přiřadit administrátora",
   "at" : "v",
+  "Attachment_File_Uploaded" : "Soubor nahrán",
   "Auth_Token" : "Auth Token",
   "Author" : "Autor",
   "Authorization_URL" : "URL autorizace",
@@ -203,6 +207,9 @@
   "Back_to_permissions" : "Zpět na práva",
   "Body" : "Obsah",
   "bold" : "tučný",
+  "bot_request" : "Request bota",
+  "BotHelpers_userFields" : "Uživatelská pole",
+  "BotHelpers_userFields_Description" : "CSV uživatelských polí, která budou přístupná botům",
   "Branch" : "Větev",
   "busy" : "zaneprázdněný",
   "Busy" : "Zaneprázdněný",
@@ -272,6 +279,8 @@
   "Custom_oauth_unique_name" : "Název vlastní OAuth",
   "Custom_Script_Logged_In" : "Vlastní skripty pro přihlášené uživatele",
   "Custom_Script_Logged_Out" : "Vlastní skripty pro odhlášené uživatele",
+  "Custom_Translations" : "Vlastní překlady",
+  "Custom_Translations_Description" : "Validní JSON obsahující slovník. Například <br/><code>{\n \"en\": {\n  \"key\": \"translation\"\n },\n \"pt\": {\n  \"key\": \"tradução\"\n }\n}</code>\n",
   "Dashboard" : "Hlavní panel",
   "Date" : "Datum",
   "days" : "dny",
@@ -423,6 +432,7 @@
   "Features_Enabled" : "Funkce Povoleny",
   "Field" : "Pole",
   "Field_removed" : "Pole odebráno",
+  "Field_required" : "Pole vyžadováno",
   "File_exceeds_allowed_size_of_bytes" : "Soubor překračuje povolenou velikost __size__ bajtů",
   "FileUpload" : "Nahrání souboru",
   "FileUpload_Enabled" : "Nahrávání souborů povoleno",
@@ -491,6 +501,14 @@
   "How_satisfied_were_you_with_this_chat" : "Jak jste byli celkovÄ› spokojeni?",
   "If_you_are_sure_type_in_your_password" : "Pokud si jste jisti, zadejte své heslo:",
   "If_you_are_sure_type_in_your_username" : "Pokud jste si jisti, zadejte své uživatelské jméno:",
+  "Iframe_Integration_receive_enable" : "Povolit příjem",
+  "Iframe_Integration_receive_enable_Description" : "Povolit původnímu oknu odesílat požadavky na Rocket.Chat",
+  "Iframe_Integration_receive_origin" : "Povolené domény",
+  "Iframe_Integration_receive_origin_Description" : "Jen stránky z povolených domén můžou odesílat požadavky. `*` pro povolení všech domén. Lze vložit více hodnot oddělených `,`. Například `http://localhost,https://localhost`",
+  "Iframe_Integration_send_enable" : "Povolit odeslání",
+  "Iframe_Integration_send_enable_Description" : "Odesílat události do původního okna",
+  "Iframe_Integration_send_target_origin" : "Odesílat cílovou doménu",
+  "Iframe_Integration_send_target_origin_Description" : "Jen stránky z povolených domén můžou čekat na události. `*` pro povolení všech domén. Lze vložit více hodnot oddělených `,`. Například `http://localhost,https://localhost`",
   "Importer_Archived" : "Archivováno",
   "Importer_done" : "Import dokončen!",
   "Importer_finishing" : "Dokončení importu.",
@@ -614,6 +632,8 @@
   "LDAP_Encryption_Description" : "Metoda šifrování používaná k zabezpečené komunikace se serverem LDAP. Jako příklady lze uvést `plain` (bez šifrování),` SSL/LDAPS` (šifrovaný od začátku), a `StartTLS` (Šifrovaná komunikaci až po připojení).",
   "LDAP_Host" : "Hostitel",
   "LDAP_Host_Description" : "Hostitel LDAP, například `ldap.example.com` nebo 10.0.0.30`.",
+  "LDAP_Import_Users" : "Importovat LDAP uživatele",
+  "LDAP_Import_Users_Description" : "Pokud povoleno, všichni LDAP uživatelé budou naimportováni<br /> *POZOR* Nastavte filtr pokud chcete omezit počet uživatelů.",
   "LDAP_Merge_Existing_Users" : "Sloučit existující uživatele",
   "LDAP_Merge_Existing_Users_Description" : "*Upozornění!* Pokud importujete uživatele z LDAP a uživatel se stejným jménem již existuje, LDAP údaje a heslo budou přiřazeny tomuto uživateli",
   "LDAP_Port" : "Port",
@@ -694,6 +714,7 @@
   "Markdown_Headers" : "Markdownu nadpisy",
   "Markdown_SupportSchemesForLink" : "Schémata používaná pro automatické odkazy markdown",
   "Markdown_SupportSchemesForLink_Description" : "Čárkami oddělený seznam povolených schémat",
+  "Max_length_is" : "Maximální délka je %s",
   "Members_List" : "Seznam členů",
   "Mentions" : "Zmínky",
   "Mentions_default" : "Zmínky (výchozí)",
@@ -744,6 +765,7 @@
   "Meta_language" : "Jazyk",
   "Meta_msvalidate01" : "MSValidate.01",
   "Meta_robots" : "Roboti",
+  "Min_length_is" : "Minimální délka je %s",
   "minutes" : "minuty",
   "Mobile_push" : "Mobilní notifikace",
   "Monitor_history_for_changes_on" : "Sledovat historii na změny:",
@@ -1018,6 +1040,7 @@
   "Settings" : "Nastavení",
   "Settings_updated" : "Nastavení aktualizováno",
   "Share_Location_Title" : "Sdílet polohu",
+  "Shared_Location" : "Sdílená lokalita",
   "Should_be_a_URL_of_an_image" : "Měla by být URL obrázku.",
   "Should_exists_a_user_with_this_username" : "Uživatel musí již existovat.",
   "Show_all" : "Ukázat vše",
@@ -1138,6 +1161,7 @@
   "theme-color-tertiary-font-color" : "Terciární Barva písma",
   "theme-color-unread-notification-color" : "Barva Nepřečtených upozornění",
   "theme-custom-css" : "Vlastní CSS",
+  "theme-font-body-font-family" : "Font obsahu",
   "There_are_no_agents_added_to_this_department_yet" : "Neexistují žádní operátoři v tomto oddělení",
   "There_are_no_integrations" : "Neexistují žádná integrace",
   "There_are_no_users_in_this_role" : "Pro tuto roli nejsou přiřazení žádní uživatelé",
@@ -1293,6 +1317,7 @@
   "You_have_been_muted" : "Byli jste ztišeni a nemůžete v této místnosti mluvit",
   "You_have_not_verified_your_email" : "Neověřili jste svůj e-mail.",
   "You_have_successfully_unsubscribed" : "Úspěšně jste se odhlásili z našeho seznamu.",
+  "You_must_join_to_view_messages_in_this_channel" : "Pro zobrazení zpráv v této místnosti je třeba se připojit",
   "You_need_confirm_email" : "Pro přihlášení nejprve potvrďte svůj e-mail!",
   "You_need_install_an_extension_to_allow_screen_sharing" : "Musíte nainstalovat rozšíření pro sdílení obrazovky",
   "You_need_to_change_your_password" : "Musíte změnit své heslo",
diff --git a/packages/rocketchat-lib/i18n/en.i18n.json b/packages/rocketchat-lib/i18n/en.i18n.json
index 3f28d74114fe9bfa95792fa631e6c2c9244d5e8f..26a9f9083cbf14efef7af3e570b728cdc49c8189 100644
--- a/packages/rocketchat-lib/i18n/en.i18n.json
+++ b/packages/rocketchat-lib/i18n/en.i18n.json
@@ -146,9 +146,10 @@
   "Animals_and_Nature" : "Animals & Nature",
   "API" : "API",
   "API_Analytics" : "Analytics",
-  "API_Embed" : "Embed",
+  "API_Embed" : "Embed Link Previews",
+  "API_Embed_Description": "Whether embedded link previews are enabled or not when a user posts a link to a website.",
   "API_EmbedDisabledFor" : "Disable Embed for Users",
-  "API_EmbedDisabledFor_Description" : "Comma-separated list of usernames",
+  "API_EmbedDisabledFor_Description" : "Comma-separated list of usernames to disable the embedded link previews.",
   "API_EmbedIgnoredHosts" : "Embed Ignored Hosts",
   "API_EmbedIgnoredHosts_Description" : "Comma-separated list of hosts or CIDR addresses, eg. localhost, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16",
   "API_EmbedSafePorts" : "Safe Ports",
@@ -346,6 +347,7 @@
   "Empty_title" : "Empty title",
   "Enable" : "Enable",
   "Enable_Desktop_Notifications" : "Enable Desktop Notifications",
+  "Office_Hours_Enabled" : "Office Hours Enabled",
   "Enabled" : "Enabled",
   "Encrypted_message" : "Encrypted message",
   "End_OTR" : "End OTR",
@@ -424,6 +426,9 @@
   "error-you-are-last-owner" : "You are the last owner. Please set new owner before leaving the room.",
   "Error_changing_password" : "Error changing password",
   "Esc_to" : "Esc to",
+  "every_30_minutes": "Once every 30 minutes",
+  "every_hour": "Once every hour",
+  "every_six_hours": "Once every six hours",
   "Example_s" : "Example: <code class=\"inline\">%s</code>",
   "Exclude_Botnames" : "Exclude bots",
   "Exclude_Botnames_Description" : "Do not propagate messages from bots whose name matches the regular expression above. If left empty, all messages from bots will be propagated.",
@@ -468,6 +473,7 @@
   "Forward_to_department" : "Forward to department",
   "Forward_to_user" : "Forward to user",
   "Frequently_Used" : "Frequently Used",
+  "Friday" : "Friday",
   "From" : "From",
   "From_Email" : "From Email",
   "From_email_warning" : "<b>Warning</b>: The field <b>From</b> is subject to your mail server settings.",
@@ -496,6 +502,7 @@
   "History" : "History",
   "Host" : "Host",
   "hours" : "hours",
+  "Hours" : "Hours",
   "How_friendly_was_the_chat_agent" : "How friendly was the chat agent?",
   "How_knowledgeable_was_the_chat_agent" : "How knowledgeable was the chat agent?",
   "How_responsive_was_the_chat_agent" : "How responsive was the chat agent?",
@@ -633,10 +640,12 @@
   "LDAP_Encryption_Description" : "The encryption method used to secure communications to the LDAP server. Examples include `plain` (no encryption), `SSL/LDAPS` (encrypted from the start), and `StartTLS` (upgrade to encrypted communication once connected).",
   "LDAP_Host" : "Host",
   "LDAP_Host_Description" : "The LDAP host, e.g. `ldap.example.com` or `10.0.0.30`.",
-  "LDAP_Merge_Existing_Users" : "Merge existing users",
-  "LDAP_Merge_Existing_Users_Description" : "*Caution!* When importing an user from LDAP and an user with same username already exists the LDAP info and password will be set into the existing user.",
   "LDAP_Import_Users" : "Import LDAP users",
   "LDAP_Import_Users_Description" : "It True sync process will be import all LDAP users <br/> *Caution!* Specify search filter to not import excess users.",
+  "LDAP_Login_Fallback" : "Login Fallback",
+  "LDAP_Login_Fallback_Description" : "If the login on LDAP is not successfull try to login in default/local account system. Helps when the LDAP is down for some reason.",
+  "LDAP_Merge_Existing_Users" : "Merge existing users",
+  "LDAP_Merge_Existing_Users_Description" : "*Caution!* When importing an user from LDAP and an user with same username already exists the LDAP info and password will be set into the existing user.",
   "LDAP_Port" : "Port",
   "LDAP_Port_Description" : "Port to access LDAP. eg: `389` or `636` for LDAPS",
   "LDAP_Reject_Unauthorized" : "Reject Unauthorized",
@@ -770,6 +779,7 @@
   "minutes" : "minutes",
   "Mobile_push" : "Mobile push",
   "Monitor_history_for_changes_on" : "Monitor history for changes on",
+  "Monday" : "Monday",
   "More_channels" : "More channels",
   "More_direct_messages" : "More direct messages",
   "More_groups" : "More private groups",
@@ -830,6 +840,7 @@
   "Off" : "Off",
   "Off_the_record_conversation" : "Off-the-record Conversation",
   "Off_the_record_conversation_is_not_available_for_your_browser_or_device" : "Off-the-record conversation is not available for your browser or device.",
+  "Office_Hours": "Office Hours",
   "Offline" : "Offline",
   "Offline_DM_Email" : "You have been direct messaged by __user__",
   "Offline_form" : "Offline form",
@@ -845,6 +856,7 @@
   "Oops!" : "Oops",
   "Open" : "Open",
   "Open_Livechats" : "Open Livechats",
+  "Open_days_of_the_week" : "Open Days of the Week",
   "Opened" : "Opened",
   "Opens_a_channel_group_or_direct_message" : "Opens a channel, group or direct message",
   "optional" : "optional",
@@ -997,6 +1009,7 @@
   "SAML_Custom_Issuer" : "Custom Issuer",
   "SAML_Custom_Provider" : "Custom Provider",
   "Sandstorm_Powerbox_Share" : "Share a Sandstorm grain",
+  "Saturday" : "Saturday",
   "Save" : "Save",
   "Save_changes" : "Save changes",
   "Save_Mobile_Bandwidth" : "Save Mobile Bandwidth",
@@ -1062,9 +1075,9 @@
   "Site_Url" : "Site URL",
   "Site_Url_Description" : "Example: https://chat.domain.com/",
   "Skip" : "Skip",
-  "SlackBridge_got_an_error_while_importing_your_messages_at_s__s_" : "SlackBridge got an error while importing your messages at %s: %s",
-  "SlackBridge_has_finished_importing_your_messages_at_s_" : "SlackBridge has finished importing your messages at %s",
-  "SlackBridge_is_importing_your_messages_at_s_" : "SlackBridge is importing your messages at %s",
+  "SlackBridge_error" : "SlackBridge got an error while importing your messages at %s: %s",
+  "SlackBridge_finish" : "SlackBridge has finished importing the messages at %s. Please reload to view all messages.",
+  "SlackBridge_start" : "@%s has started a SlackBridge import at `#%s`. We'll let you know when it's finished.",
   "Slash_Gimme_Description" : "Displays ༼ つ ◕_◕ ༽つ before your message",
   "Slash_LennyFace_Description" : "Displays ( ͡° ͜ʖ ͡°) after your message",
   "Slash_Shrug_Description" : "Displays ¯\\_(ツ)_/¯ after your message",
@@ -1072,6 +1085,14 @@
   "Slash_TableUnflip_Description" : "Displays ┬─┬ ノ( ゜-゜ノ)",
   "Slash_Topic_Description" : "Set topic",
   "Slash_Topic_Params" : "Topic message",
+  "Smarsh_Enabled": "Smarsh Enabled",
+  "Smarsh_Enabled_Description": "Whether the Smarsh eml connector is enabled or not (needs 'From Email' filled in under Email -> SMTP).",
+  "Smarsh_Email": "Smarsh Email",
+  "Smarsh_Email_Description": "Smarsh Email Address to send the .eml file to.",
+  "Smarsh_Interval": "Smarsh Interval",
+  "Smarsh_Interval_Description": "The amount of time to wait before sending the chats (needs 'From Email' filled in under Email -> SMTP).",
+  "Smarsh_MissingEmail_Email": "Missing Email",
+  "Smarsh_MissingEmail_Email_Description": "The email to show for a user account when their email address is missing, generally happens with bot accounts.",
   "Smileys_and_People" : "Smileys & People",
   "SMS_Enabled" : "SMS Enabled",
   "SMTP" : "SMTP",
@@ -1114,6 +1135,7 @@
   "Submit" : "Submit",
   "Success" : "Success",
   "Success_message" : "Success message",
+  "Sunday" : "Sunday",
   "Survey" : "Survey",
   "Survey_instructions" : "Rate each question according to your satisfaction, 1 meaning you are completely unsatisfied and 5 meaning you are completely satisfied.",
   "Symbols" : "Symbols",
@@ -1177,6 +1199,7 @@
   "This_is_a_push_test_messsage" : "This is a push test messsage",
   "This_room_has_been_archived_by__username_" : "This room has been archived by __username__",
   "This_room_has_been_unarchived_by__username_" : "This room has been unarchived by __username__",
+  "Thursday" : "Thursday",
   "Time_in_seconds" : "Time in seconds",
   "Title" : "Title",
   "Title_bar_color" : "Title bar color",
@@ -1191,6 +1214,7 @@
   "Trigger_Words" : "Trigger Words",
   "Triggers" : "Triggers",
   "True" : "True",
+  "Tuesday" : "Tuesday",
   "Type" : "Type",
   "Type_your_email" : "Type your email",
   "Type_your_message" : "Type your message",
@@ -1300,6 +1324,7 @@
   "WebRTC_Enable_Private" : "Enable for Private Channels",
   "WebRTC_Servers" : "STUN/TURN Servers",
   "WebRTC_Servers_Description" : "A list of STUN and TURN servers separated by comma.<br/>Username, password and port are allowed in the format `username:password@stun:host:port` or `username:password@turn:host:port`.",
+  "Wednesday" : "Wednesday",
   "Welcome" : "Welcome <em>%s</em>.",
   "Welcome_to_the" : "Welcome to the",
   "Why_do_you_want_to_report_question_mark" : "Why do you want to report?",
@@ -1324,6 +1349,7 @@
   "You_have_been_muted" : "You have been muted and cannot speak in this room",
   "You_have_not_verified_your_email" : "You have not verified your email.",
   "You_have_successfully_unsubscribed" : "You have successfully unsubscribed from our Mailling List.",
+  "You_must_join_to_view_messages_in_this_channel" : "You must join to view messages in this channel",
   "You_need_confirm_email" : "You need to confirm your email to login!",
   "You_need_install_an_extension_to_allow_screen_sharing" : "You need install an extension to allow screen sharing",
   "You_need_to_change_your_password" : "You need to change your password",
@@ -1342,4 +1368,4 @@
   "Your_mail_was_sent_to_s" : "Your mail was sent to %s",
   "Your_password_is_wrong" : "Your password is wrong!",
   "Your_push_was_sent_to_s_devices" : "Your push was sent to %s devices"
-}
\ No newline at end of file
+}
diff --git a/packages/rocketchat-lib/i18n/fr.i18n.json b/packages/rocketchat-lib/i18n/fr.i18n.json
index d4ce3cd5546f72c84295f78312fa08a28a5d0a27..722d059b34d017c7550da839b05f6412d066f085 100644
--- a/packages/rocketchat-lib/i18n/fr.i18n.json
+++ b/packages/rocketchat-lib/i18n/fr.i18n.json
@@ -29,6 +29,8 @@
   "Accounts_BlockedDomainsList_Description" : "Liste de domaines bloqués, séparés par des virgules",
   "Accounts_BlockedUsernameList" : "Liste des noms d'utilisateurs bloqués",
   "Accounts_BlockedUsernameList_Description" : "Liste de noms d'utilisateurs bloqués (insensible à la casse), séparée par des virgules",
+  "Accounts_CustomFields" : "Traductions personnalisées",
+  "Accounts_CustomFields_Description" : "Devrait être un JSON valide où les clés sont les noms des champs contenant un dictionnaire de champs de paramètrage. Exemple :</br>\n<code>{\n \"role\": {\n  \"type\": \"select\",\n  \"defaultValue\": \"eleve\",\n  \"options\": [\"enseignant\", \"eleve\"],\n  \"required\": true,\n  \"modifyRecordField\": {\n   \"array\": true,\n   \"field\": \"roles\"\n  }\n },\n \"twitter\": {\n  \"type\": \"text\",\n  \"required\": true,\n  \"minLength\": 2,\n  \"maxLength\": 10\n }\n}</code> \n",
   "Accounts_denyUnverifiedEmail" : "Refuser les e-mails non vérifiés",
   "Accounts_EmailVerification" : "Vérification de l'adresse e-mail",
   "Accounts_EmailVerification_Description" : "Vous devez avoir des paramètres SMTP corrects pour utiliser cette fonctionnalité",
@@ -100,6 +102,7 @@
   "Accounts_RegistrationForm_SecretURL" : "URL secrète du formulaire d'inscription",
   "Accounts_RegistrationForm_SecretURL_Description" : "Vous devez fournir une chaîne de caractères aléatoire qui sera ajoutée à votre URL d'inscription. Exemple: https://demo.rocket.chat/register/[secret_hash]",
   "Accounts_RequireNameForSignUp" : "Exiger un nom pour s'inscrire",
+  "Accounts_RequirePasswordConfirmation" : "Confirmation du mot de passe requise",
   "Accounts_ShowFormLogin" : "Afficher le formulaire de connexion",
   "Accounts_UseDefaultBlockedDomainsList" : "Utiliser la liste de domaines bloqués par défaut ",
   "Accounts_UseDNSDomainCheck" : "Utiliser la vérification de Domaine du DNS",
@@ -170,6 +173,7 @@
   "Are_you_sure_you_want_to_delete_your_account" : "Êtes-vous sûr(e) de vouloir supprimer votre compte ?",
   "Assign_admin" : "Attribution d'un administrateur",
   "at" : "à",
+  "Attachment_File_Uploaded" : "Fichier envoyé",
   "Auth_Token" : "Jeton d'Auth",
   "Author" : "Auteur",
   "Authorization_URL" : "URL d’autorisation",
@@ -275,6 +279,8 @@
   "Custom_oauth_unique_name" : "Nom unique de l'OAuth personnalisé",
   "Custom_Script_Logged_In" : "Script personnalisé pour les utilisateurs connectés",
   "Custom_Script_Logged_Out" : "Script personnalisé pour les utilisateurs déconnectés",
+  "Custom_Translations" : "Traductions personnalisées",
+  "Custom_Translations_Description" : "Devrait être un JSON valide où les clefs sont des langues conenant un dictionnaire de clefs et de traductions.\nExemple:</br><code>{\n \"en\": {\n  \"key\": \"translation\"\n },\n \"fr\": {\n  \"key\": \"traduction\"\n }\n}</code> ",
   "Dashboard" : "Tableau de bord",
   "Date" : "Date",
   "days" : "jours",
@@ -426,6 +432,7 @@
   "Features_Enabled" : "Caractéristiques Enabled",
   "Field" : "Champ",
   "Field_removed" : "Champ supprimé",
+  "Field_required" : "Champs requis",
   "File_exceeds_allowed_size_of_bytes" : "Le fichier dépasse la taille maximale autorisée : __size__ octets",
   "FileUpload" : "Envoi de fichiers",
   "FileUpload_Enabled" : "Envois de fichiers activés",
@@ -625,6 +632,8 @@
   "LDAP_Encryption_Description" : "La méthode de chiffrement utilisée pour sécuriser les communications avec le serveur LDAP. Parmi les exemples on trouve `plain` (pas de chiffrement), `SSL/LDAPS` (chiffrement dès le début) et `StartTLS` (chiffrement une fois la connexion établie).",
   "LDAP_Host" : "Hôte",
   "LDAP_Host_Description" : "L'hôte LDAP, par exemple `ldap.exemple.com` ou `10.0.0.30`.",
+  "LDAP_Import_Users" : "Importer les utilisateurs LDAP",
+  "LDAP_Import_Users_Description" : "Si VRAI les processus de synchronisation importeront l'ensemble des utilisateurs LDAP <br/>\n\n*Attention !* Spécifiez les filtres de recherche pour ne pas importer trop d'utilisateurs.",
   "LDAP_Merge_Existing_Users" : "Fusionner les utilisateurs existants",
   "LDAP_Merge_Existing_Users_Description" : "*Attention !* Si vous importez un utilisateur depuis le LDAP et qu'un utilisateur avec le même nom existe déjà, les informations et le mot de passe du LDAP lui seront attribués.",
   "LDAP_Port" : "Port LDAP",
@@ -705,6 +714,7 @@
   "Markdown_Headers" : "Titres Markdown",
   "Markdown_SupportSchemesForLink" : "Schémas de Markdown supportés pour les liens",
   "Markdown_SupportSchemesForLink_Description" : "Liste de schémas séparés par des virgules",
+  "Max_length_is" : "La longueur maximale est %s",
   "Members_List" : "Liste des membres",
   "Mentions" : "Mentions",
   "Mentions_default" : "Mentions (défaut)",
@@ -755,6 +765,7 @@
   "Meta_language" : "Langue",
   "Meta_msvalidate01" : "MSValidate.01",
   "Meta_robots" : "Robots",
+  "Min_length_is" : "La longueur minimale est %s",
   "minutes" : "minutes",
   "Mobile_push" : "Notifications push sur mobile",
   "Monitor_history_for_changes_on" : "Surveiller l'historique des changements sur",
@@ -1029,6 +1040,7 @@
   "Settings" : "Paramètres",
   "Settings_updated" : "Paramètres mis à jour",
   "Share_Location_Title" : "Partager votre position ?",
+  "Shared_Location" : "Dossier partagé",
   "Should_be_a_URL_of_an_image" : "Doit être l'URL d'une image.",
   "Should_exists_a_user_with_this_username" : "L'utilisateur doit déjà exister",
   "Show_all" : "Afficher tout",
@@ -1305,6 +1317,7 @@
   "You_have_been_muted" : "Vous avez été rendu muet et ne pouvez donc pas parler dans ce salon",
   "You_have_not_verified_your_email" : "Vous n'avez pas vérifié votre adresse e-mail.",
   "You_have_successfully_unsubscribed" : "Vous êtes désabonné avec succès de notre liste de diffusion.",
+  "You_must_join_to_view_messages_in_this_channel" : "Vous devez rejoindre ce canal pour voir les messages",
   "You_need_confirm_email" : "Vous devez confirmer votre adresse email pour vous connecter !",
   "You_need_install_an_extension_to_allow_screen_sharing" : "Vous devez installer une extension pour permettre le partage d'écran",
   "You_need_to_change_your_password" : "Vous devez changer votre mot de passe.",
diff --git a/packages/rocketchat-lib/i18n/it.i18n.json b/packages/rocketchat-lib/i18n/it.i18n.json
index 149f284a5e23fef28045c17b1c7f1ecc183efa99..4de30e0f97e719867e34a89e4e28698f5b998aa4 100644
--- a/packages/rocketchat-lib/i18n/it.i18n.json
+++ b/packages/rocketchat-lib/i18n/it.i18n.json
@@ -632,6 +632,8 @@
   "LDAP_Encryption_Description" : "Il metodo di cifratura utilizzato per proteggere le comunicazioni con il server LDAP. Gli esempi includono `plain` (senza crittografia),` SSL / LDAPS` (criptato dall'inizio), e `StartTLS` (passaggio ad una comunicazione criptata una volta connesso).",
   "LDAP_Host" : "Host",
   "LDAP_Host_Description" : "Host LDAP, ad esempio `ldap.example.com` o` 10.0.0.30`.",
+  "LDAP_Import_Users" : "Importa utenti LDAP",
+  "LDAP_Import_Users_Description" : "Il processo true sync importerà tutti gli utenti LDAP<br/> *Attenzione!* Specificare il filtro di ricerca per non importare gli utenti in eccesso.",
   "LDAP_Merge_Existing_Users" : "Unisci gli utenti esistenti",
   "LDAP_Merge_Existing_Users_Description" : "*Attenzione* Quando importi un utente da LDAP e un utente con lo stesso username esiste già le informazioni LDRP e la password saranno impostate sull'utente già esistente.",
   "LDAP_Port" : "Porta",
@@ -1315,6 +1317,7 @@
   "You_have_been_muted" : "Hai silenziato e non puoi parlare in questa stanza",
   "You_have_not_verified_your_email" : "Non hai verificato la tua email.",
   "You_have_successfully_unsubscribed" : "Sei stato disiscritto con successo dalla nostra Mailing List.",
+  "You_must_join_to_view_messages_in_this_channel" : "Devi entrare nel canale per poter vedere i messaggi contenuti",
   "You_need_confirm_email" : "Devi confermare la tua email per accedere!",
   "You_need_install_an_extension_to_allow_screen_sharing" : "Devi installare una estensione per permettere la condivisione dello schermo",
   "You_need_to_change_your_password" : "Devi cambiare la tua password",
diff --git a/packages/rocketchat-lib/i18n/tr.i18n.json b/packages/rocketchat-lib/i18n/tr.i18n.json
index 91dc32896021e5c5361d8b138c468eb7dcb96e4a..b00ab162ab7a2618ca921debb8dbe4d9c071a13d 100644
--- a/packages/rocketchat-lib/i18n/tr.i18n.json
+++ b/packages/rocketchat-lib/i18n/tr.i18n.json
@@ -459,6 +459,10 @@
   "Force_SSL" : "kuvvet SSL",
   "Force_SSL_Description" : "* Dikkat! * _Force SSL_ ters proxy ile asla kullanılmamalıdır. Eğer ters proxy varsa, ORADA yönlendirme yapmak gerekir. Bu seçenek, ters vekil de yönlendirme yapılandırması izin vermez Heroku gibi dağıtımlar için var.",
   "Forgot_password" : "Åžifrenizi mi unuttunuz",
+  "Forward" : "Ä°let",
+  "Forward_chat" : "Sohbeti Ä°let",
+  "Forward_to_department" : "Bölüme İlet",
+  "Forward_to_user" : "Kullanıcıya İlet",
   "Frequently_Used" : "Sıklıkla kullanılan",
   "From" : "itibaren",
   "From_Email" : "E-posta Gönderen",
@@ -470,10 +474,13 @@
   "Global" : "global",
   "GoogleSiteVerification_id" : "Google Site DoÄŸrulama KimliÄŸi",
   "GoogleTagManager_id" : "Google Etiket Yöneticisi Kimliği",
+  "Guest_Pool" : "Misafir Havuzu",
   "Has_more" : "Daha fazla var",
   "Hash" : "esrar",
   "Header" : "üstbilgi",
   "Hidden" : "Gizli",
+  "Hide_Avatars" : "Profil Resimlerini Gizle",
+  "Hide_flextab" : "Tıklayarak Sağ Taraftaki Barı Gizle",
   "Hide_Group_Warning" : "EÄŸer grup \" %s\" gizlemek istediÄŸinizden emin misiniz?",
   "Hide_Private_Warning" : "Eğer \" %s\" ile tartışma gizlemek istediğiniz emin misiniz?",
   "Hide_room" : "Odayı gizle",
@@ -491,6 +498,10 @@
   "How_satisfied_were_you_with_this_chat" : "Bu sohbet ile ne kadar memnun kaldınız?",
   "If_you_are_sure_type_in_your_password" : "Şifrenizin emin türü ise:",
   "If_you_are_sure_type_in_your_username" : "Eğer kullanıcı adınızı emin türü ise:",
+  "Iframe_Integration_receive_enable" : "Almayı Etkinleştir",
+  "Iframe_Integration_receive_enable_Description" : "Ata Pencere'nin Rocket Chat'e Komutlar Göndermesine İzin Ver",
+  "Iframe_Integration_send_enable" : "Göndermeyi Etkinleştir",
+  "Iframe_Integration_send_enable_Description" : "Olayları Ata Pencere'ye Gönder",
   "Importer_Archived" : "arÅŸivlenen",
   "Importer_done" : "Alma tamamlandığı!",
   "Importer_finishing" : "ithalat kadar bitirme.",
@@ -508,6 +519,7 @@
   "Importer_Prepare_Uncheck_Deleted_Users" : "Işaretini kaldırın Silinmiş Kullanıcıları",
   "Importer_progress_error" : "ithalat için ilerleme alınamadı.",
   "Importer_setup_error" : "ileti kurarken bir hata oluÅŸtu.",
+  "Incoming_Livechats" : "Gelen Canlı Sohbetler",
   "inline_code" : "kod_satırı",
   "Install_Extension" : "Uzantısı yükleyin",
   "Install_FxOs" : "Firefox üzerinde Rocket.Chat yükleyin",
@@ -613,6 +625,8 @@
   "LDAP_Encryption_Description" : "LDAP sunucusuna iletişim güvenliğini sağlamak için kullanılan şifreleme yöntemi. Örnekler `plain` (hiçbir şifreleme), (baştan şifreli)` SSL / LDAPS` ve `StartTLS` (şifreli iletişim için bir kez bağlı yükseltme) içerir.",
   "LDAP_Host" : "evsahibi",
   "LDAP_Host_Description" : "LDAP ana örneğin 'ldap.example.com` ya da' 10.0.0.30`.",
+  "LDAP_Import_Users" : "LDAP Kullanıcılarını İçe Aktar",
+  "LDAP_Merge_Existing_Users" : "Varolan Kullanıcıları Birleştir",
   "LDAP_Port" : "LDAP Port",
   "LDAP_Port_Description" : "Liman LDAP erişmek için. LDAPS için `389` ya da '636`: örneğin",
   "LDAP_Reject_Unauthorized" : "Yetkisiz Reddet",
diff --git a/packages/rocketchat-lib/package.js b/packages/rocketchat-lib/package.js
index 8b7ca207f3ff0bb4e0fc8bf308741aa65dd0cce6..143145d5b550be836195148db4f88ed099f5c39b 100644
--- a/packages/rocketchat-lib/package.js
+++ b/packages/rocketchat-lib/package.js
@@ -32,6 +32,7 @@ Package.onUse(function(api) {
 	api.use('rocketchat:streamer');
 	api.use('rocketchat:version');
 	api.use('rocketchat:logger');
+	api.use('rocketchat:custom-oauth');
 
 	api.use('templating', 'client');
 	api.use('kadira:flow-router');
@@ -56,13 +57,22 @@ Package.onUse(function(api) {
 	api.addFiles('server/lib/RateLimiter.coffee', 'server');
 
 	// SERVER FUNCTIONS
+	api.addFiles('server/functions/addUserToDefaultChannels.js', 'server');
+	api.addFiles('server/functions/addUserToRoom.js', 'server');
+	api.addFiles('server/functions/archiveRoom.js', 'server');
 	api.addFiles('server/functions/checkUsernameAvailability.coffee', 'server');
 	api.addFiles('server/functions/checkEmailAvailability.js', 'server');
+	api.addFiles('server/functions/createRoom.js', 'server');
+	api.addFiles('server/functions/deleteMessage.js', 'server');
 	api.addFiles('server/functions/deleteUser.js', 'server');
+	api.addFiles('server/functions/removeUserFromRoom.js', 'server');
 	api.addFiles('server/functions/sendMessage.coffee', 'server');
 	api.addFiles('server/functions/settings.coffee', 'server');
+	api.addFiles('server/functions/setUserAvatar.js', 'server');
 	api.addFiles('server/functions/setUsername.coffee', 'server');
 	api.addFiles('server/functions/setEmail.js', 'server');
+	api.addFiles('server/functions/unarchiveRoom.js', 'server');
+	api.addFiles('server/functions/updateMessage.js', 'server');
 	api.addFiles('server/functions/Notifications.coffee', 'server');
 
 	// SERVER LIB
@@ -88,14 +98,21 @@ Package.onUse(function(api) {
 
 	// SERVER METHODS
 	api.addFiles('server/methods/addOAuthService.coffee', 'server');
+	api.addFiles('server/methods/addUserToRoom.coffee', 'server');
+	api.addFiles('server/methods/archiveRoom.coffee', 'server');
 	api.addFiles('server/methods/checkRegistrationSecretURL.coffee', 'server');
+	api.addFiles('server/methods/createChannel.coffee', 'server');
+	api.addFiles('server/methods/createPrivateGroup.coffee', 'server');
+	api.addFiles('server/methods/deleteMessage.coffee', 'server');
 	api.addFiles('server/methods/deleteUserOwnAccount.js', 'server');
 	api.addFiles('server/methods/getRoomRoles.js', 'server');
 	api.addFiles('server/methods/getUserRoles.js', 'server');
+	api.addFiles('server/methods/joinRoom.coffee', 'server');
 	api.addFiles('server/methods/joinDefaultChannels.coffee', 'server');
+	api.addFiles('server/methods/leaveRoom.coffee', 'server');
 	api.addFiles('server/methods/removeOAuthService.coffee', 'server');
 	api.addFiles('server/methods/robotMethods.coffee', 'server');
-	api.addFiles('server/methods/saveSetting.coffee', 'server');
+	api.addFiles('server/methods/saveSetting.js', 'server');
 	api.addFiles('server/methods/sendInvitationEmail.coffee', 'server');
 	api.addFiles('server/methods/sendMessage.coffee', 'server');
 	api.addFiles('server/methods/sendSMTPTestEmail.coffee', 'server');
@@ -105,6 +122,8 @@ Package.onUse(function(api) {
 	api.addFiles('server/methods/insertOrUpdateUser.coffee', 'server');
 	api.addFiles('server/methods/setEmail.js', 'server');
 	api.addFiles('server/methods/restartServer.coffee', 'server');
+	api.addFiles('server/methods/unarchiveRoom.coffee', 'server');
+	api.addFiles('server/methods/updateMessage.coffee', 'server');
 	api.addFiles('server/methods/filterBadWords.js', ['server']);
 	api.addFiles('server/methods/filterATAllTag.js', 'server');
 
diff --git a/packages/rocketchat-lib/rocketchat.info b/packages/rocketchat-lib/rocketchat.info
index e79a5d15db7cc71a2c9bf3a55b42a67644b2fa3f..b9e103377944f144fe3d53581a154762306cb61b 100644
--- a/packages/rocketchat-lib/rocketchat.info
+++ b/packages/rocketchat-lib/rocketchat.info
@@ -1,3 +1,3 @@
 {
-	"version": "0.37.1"
+	"version": "0.38.0"
 }
diff --git a/packages/rocketchat-lib/server/functions/addUserToDefaultChannels.js b/packages/rocketchat-lib/server/functions/addUserToDefaultChannels.js
new file mode 100644
index 0000000000000000000000000000000000000000..a520ff941f59a406b45d3402ba046efbbbc7449c
--- /dev/null
+++ b/packages/rocketchat-lib/server/functions/addUserToDefaultChannels.js
@@ -0,0 +1,25 @@
+RocketChat.addUserToDefaultChannels = function(user, silenced) {
+	RocketChat.callbacks.run('beforeJoinDefaultChannels', user);
+	let defaultRooms = RocketChat.models.Rooms.findByDefaultAndTypes(true, ['c', 'p'], {fields: {usernames: 0}}).fetch();
+	defaultRooms.forEach((room) => {
+
+		// put user in default rooms
+		RocketChat.models.Rooms.addUsernameById(room._id, user.username);
+
+		if (!RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(room._id, user._id)) {
+
+			// Add a subscription to this user
+			RocketChat.models.Subscriptions.createWithRoomAndUser(room, user, {
+				ts: new Date(),
+				open: true,
+				alert: true,
+				unread: 1
+			});
+
+			// Insert user joined message
+			if (!silenced) {
+				RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(room._id, user);
+			}
+		}
+	});
+};
diff --git a/packages/rocketchat-lib/server/functions/addUserToRoom.js b/packages/rocketchat-lib/server/functions/addUserToRoom.js
new file mode 100644
index 0000000000000000000000000000000000000000..f5f9424c2e04703095880089bcffb3bd9038a388
--- /dev/null
+++ b/packages/rocketchat-lib/server/functions/addUserToRoom.js
@@ -0,0 +1,45 @@
+RocketChat.addUserToRoom = function(rid, user, inviter, silenced) {
+	let now = new Date();
+	let room = RocketChat.models.Rooms.findOneById(rid);
+
+	// Check if user is already in room
+	let subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(rid, user._id);
+	if (subscription) {
+		return;
+	}
+
+	if (room.t === 'c') {
+		RocketChat.callbacks.run('beforeJoinRoom', user, room);
+	}
+
+	var muted = room.ro && !RocketChat.authz.hasPermission(user._id, 'post-read-only');
+	RocketChat.models.Rooms.addUsernameById(rid, user.username, muted);
+	RocketChat.models.Subscriptions.createWithRoomAndUser(room, user, {
+		ts: now,
+		open: true,
+		alert: true,
+		unread: 1
+	});
+
+	if (!silenced) {
+		if (inviter) {
+			RocketChat.models.Messages.createUserAddedWithRoomIdAndUser(rid, user, {
+				ts: now,
+				u: {
+					_id: inviter._id,
+					username: inviter.username
+				}
+			});
+		} else {
+			RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(rid, user, { ts: now });
+		}
+	}
+
+	if (room.t === 'c') {
+		Meteor.defer(function() {
+			RocketChat.callbacks.run('afterJoinRoom', user, room);
+		});
+	}
+
+	return true;
+};
diff --git a/packages/rocketchat-lib/server/functions/archiveRoom.js b/packages/rocketchat-lib/server/functions/archiveRoom.js
new file mode 100644
index 0000000000000000000000000000000000000000..ef2aafeffe47aea1e2b839be8c6c011a4332ab35
--- /dev/null
+++ b/packages/rocketchat-lib/server/functions/archiveRoom.js
@@ -0,0 +1,4 @@
+RocketChat.archiveRoom = function(rid) {
+	RocketChat.models.Rooms.archiveById(rid);
+	RocketChat.models.Subscriptions.archiveByRoomId(rid);
+};
diff --git a/packages/rocketchat-lib/server/functions/createPrivateGroup.js b/packages/rocketchat-lib/server/functions/createPrivateGroup.js
new file mode 100644
index 0000000000000000000000000000000000000000..3038bac2315fcdd9f484fb2ec146217670dce505
--- /dev/null
+++ b/packages/rocketchat-lib/server/functions/createPrivateGroup.js
@@ -0,0 +1,65 @@
+/* globals RocketChat */
+RocketChat.createPrivateGroup = function(name, owner, members) {
+	name = s.trim(name);
+	owner = s.trim(owner);
+	members = [].concat(members);
+
+	if (!name) {
+		throw new Meteor.Error('error-invalid-name', 'Invalid name', { function: 'RocketChat.createPrivateGroup' });
+	}
+
+	if (!owner) {
+		throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: 'RocketChat.createPrivateGroup' });
+	}
+
+	let nameValidation;
+	try {
+		nameValidation = new RegExp('^' + RocketChat.settings.get('UTF8_Names_Validation') + '$');
+	} catch (error) {
+		nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$');
+	}
+
+	if (!nameValidation.test(name)) {
+		throw new Meteor.Error('error-invalid-name', 'Invalid name', { function: 'RocketChat.createPrivateGroup' });
+	}
+
+	let now = new Date();
+	if (!_.contains(members, owner)) {
+		members.push(owner);
+	}
+
+	// avoid duplicate names
+	let room = RocketChat.models.Rooms.findOneByName(name);
+	if (room) {
+		if (room.archived) {
+			throw new Meteor.Error('error-archived-duplicate-name', 'There\'s an archived channel with name ' + name, { function: 'RocketChat.createPrivateGroup', room_name: name });
+		} else {
+			throw new Meteor.Error('error-duplicate-channel-name', 'A channel with name \'' + name + '\' exists', { function: 'RocketChat.createPrivateGroup', room_name: name });
+		}
+	}
+
+	room = RocketChat.models.Rooms.createWithTypeNameUserAndUsernames('p', name, owner, members, { ts: now });
+
+	for (let username of members) {
+		let member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1 }});
+		if (!member) {
+			continue;
+		}
+
+		let extra = { open: true };
+
+		if (username === owner) {
+			extra.ls = now;
+		}
+
+		RocketChat.models.Subscriptions.createWithRoomAndUser(room, member, extra);
+	}
+
+	// set owner
+	owner = RocketChat.models.Users.findOneByUsername(owner, { fields: { username: 1 }});
+	RocketChat.authz.addUserRoles(owner._id, ['owner'], room._id);
+
+	return {
+		rid: room._id
+	};
+};
diff --git a/packages/rocketchat-lib/server/functions/createRoom.js b/packages/rocketchat-lib/server/functions/createRoom.js
new file mode 100644
index 0000000000000000000000000000000000000000..e0eab0d8744e54db9171f789f59647696c94b801
--- /dev/null
+++ b/packages/rocketchat-lib/server/functions/createRoom.js
@@ -0,0 +1,95 @@
+/* globals RocketChat */
+RocketChat.createRoom = function(type, name, owner, members, readOnly) {
+	name = s.trim(name);
+	owner = s.trim(owner);
+	members = [].concat(members);
+
+	if (!name) {
+		throw new Meteor.Error('error-invalid-name', 'Invalid name', { function: 'RocketChat.createRoom' });
+	}
+
+	owner = RocketChat.models.Users.findOneByUsername(owner, { fields: { username: 1 }});
+	if (!owner) {
+		throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: 'RocketChat.createRoom' });
+	}
+
+	let nameValidation;
+	try {
+		nameValidation = new RegExp('^' + RocketChat.settings.get('UTF8_Names_Validation') + '$');
+	} catch (error) {
+		nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$');
+	}
+
+	if (!nameValidation.test(name)) {
+		throw new Meteor.Error('error-invalid-name', 'Invalid name', { function: 'RocketChat.createRoom' });
+	}
+
+	let now = new Date();
+	if (!_.contains(members, owner.username)) {
+		members.push(owner.username);
+	}
+
+	// avoid duplicate names
+	let room = RocketChat.models.Rooms.findOneByName(name);
+	if (room) {
+		if (room.archived) {
+			throw new Meteor.Error('error-archived-duplicate-name', 'There\'s an archived channel with name ' + name, { function: 'RocketChat.createRoom', room_name: name });
+		} else {
+			throw new Meteor.Error('error-duplicate-channel-name', 'A channel with name \'' + name + '\' exists', { function: 'RocketChat.createRoom', room_name: name });
+		}
+	}
+
+	if (type === 'c') {
+		RocketChat.callbacks.run('beforeCreateChannel', owner, {
+			t: 'c',
+			name: name,
+			ts: now,
+			ro: readOnly === true,
+			sysMes: readOnly !== true,
+			usernames: members,
+			u: {
+				_id: owner._id,
+				username: owner.username
+			}
+		});
+	}
+
+	room = RocketChat.models.Rooms.createWithTypeNameUserAndUsernames(type, name, owner.username, members, { 
+		ts: now,
+		ro: readOnly === true,
+		sysMes: readOnly !== true,
+	});
+
+	for (let username of members) {
+		let member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1 }});
+		if (!member) {
+			continue;
+		}
+
+		// make all room members muted by default, unless they have the post-read-only permission
+		if (readOnly === true && !RocketChat.authz.hasPermission(member._id, 'post-read-only'))
+		{
+			RocketChat.models.Rooms.muteUsernameByRoomId(room._id, username);
+		}
+
+		let extra = { open: true };
+
+		if (username === owner.username) {
+			extra.ls = now;
+		}
+
+		RocketChat.models.Subscriptions.createWithRoomAndUser(room, member, extra);
+	}
+
+	RocketChat.authz.addUserRoles(owner._id, ['owner'], room._id);
+
+	if (type === 'c') {
+		Meteor.defer(() => {
+			RocketChat.callbacks.run('afterCreateChannel', owner, room);
+		});
+	}
+
+	return {
+		rid: room._id
+	};
+};
diff --git a/packages/rocketchat-lib/server/functions/deleteMessage.js b/packages/rocketchat-lib/server/functions/deleteMessage.js
new file mode 100644
index 0000000000000000000000000000000000000000..54ac4dd917589bf9965905af321a6570f103ee27
--- /dev/null
+++ b/packages/rocketchat-lib/server/functions/deleteMessage.js
@@ -0,0 +1,31 @@
+/* globals FileUpload */
+RocketChat.deleteMessage = function(message, user) {
+	let keepHistory = RocketChat.settings.get('Message_KeepHistory');
+	let showDeletedStatus = RocketChat.settings.get('Message_ShowDeletedStatus');
+
+	if (keepHistory) {
+		if (showDeletedStatus) {
+			RocketChat.models.Messages.cloneAndSaveAsHistoryById(message._id);
+		} else {
+			RocketChat.models.Messages.setHiddenById(message._id, true);
+		}
+
+		if (message.file && message.file._id) {
+			RocketChat.models.Uploads.update(message.file._id, { $set: { _hidden: true } });
+		}
+	} else {
+		if (!showDeletedStatus) {
+			RocketChat.models.Messages.removeById(message._id);
+		}
+
+		if (message.file && message.file._id) {
+			FileUpload.delete(message.file._id);
+		}
+	}
+
+	if (showDeletedStatus) {
+		RocketChat.models.Messages.setAsDeletedByIdAndUser(message._id, user);
+	} else {
+		RocketChat.Notifications.notifyRoom(message.rid, 'deleteMessage', { _id: message._id });
+	}
+};
diff --git a/packages/rocketchat-lib/server/functions/removeUserFromRoom.js b/packages/rocketchat-lib/server/functions/removeUserFromRoom.js
new file mode 100644
index 0000000000000000000000000000000000000000..57fa231c86bb0e6f2f3cd81f35dbc5b83fd8be1c
--- /dev/null
+++ b/packages/rocketchat-lib/server/functions/removeUserFromRoom.js
@@ -0,0 +1,23 @@
+RocketChat.removeUserFromRoom = function(rid, user) {
+	let room = RocketChat.models.Rooms.findOneById(rid);
+
+	if (room) {
+		RocketChat.callbacks.run('beforeLeaveRoom', user, room);
+		RocketChat.models.Rooms.removeUsernameById(rid, user.username);
+
+		if (room.usernames.indexOf(user.username) !== -1) {
+			let removedUser = user;
+			RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(rid, removedUser);
+		}
+
+		if (room.t === 'l') {
+			RocketChat.models.Messages.createCommandWithRoomIdAndUser('survey', rid, user);
+		}
+
+		RocketChat.models.Subscriptions.removeByRoomIdAndUserId(rid, user._id);
+
+		Meteor.defer(function() {
+			RocketChat.callbacks.run('afterLeaveRoom', user, room);
+		});
+	}
+};
diff --git a/packages/rocketchat-lib/server/functions/setUserAvatar.js b/packages/rocketchat-lib/server/functions/setUserAvatar.js
new file mode 100644
index 0000000000000000000000000000000000000000..40213653b40bc0fad94abcafbb3efea49f53247a
--- /dev/null
+++ b/packages/rocketchat-lib/server/functions/setUserAvatar.js
@@ -0,0 +1,55 @@
+RocketChat.setUserAvatar = function(user, dataURI, contentType, service) {
+	if (service === 'initials') {
+		return RocketChat.models.Users.setAvatarOrigin(user._id, service);
+	}
+
+	if (service === 'url') {
+		let result = null;
+
+		try {
+			result = HTTP.get(dataURI, { npmRequestOptions: {encoding: 'binary'} });
+		} catch (error) {
+			console.log(`Error while handling the setting of the avatar from a url (${dataURI}) for ${user.username}:`, error);
+			throw new Meteor.Error('error-avatar-url-handling', `Error while handling avatar setting from a URL (${dataURI}) for ${user.username}`, { function: 'RocketChat.setUserAvatar', url: dataURI, username: user.username });
+		}
+
+		if (result.statusCode !== 200) {
+			console.log(`Not a valid response, ${result.statusCode}, from the avatar url: ${dataURI}`);
+			throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${dataURI}`, { function: 'RocketChat.setUserAvatar', url: dataURI });
+		}
+
+		if (!/image\/.+/.test(result.headers['content-type'])) {
+			console.log(`Not a valid content-type from the provided url, ${result.headers['content-type']}, from the avatar url: ${dataURI}`);
+			throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${dataURI}`, { function: 'RocketChat.setUserAvatar', url: dataURI });
+		}
+
+		let ars = RocketChatFile.bufferToStream(new Buffer(result.content, 'binary'));
+		RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${user.username}.jpg`));
+		let aws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${user.username}.jpg`), result.headers['content-type']);
+		aws.on('end', Meteor.bindEnvironment(function() {
+			Meteor.setTimeout(function() {
+				console.log(`Set ${user.username}'s avatar from the url: ${dataURI}`);
+				RocketChat.models.Users.setAvatarOrigin(user._id, service);
+				RocketChat.Notifications.notifyAll('updateAvatar', { username: user.username });
+			}, 500);
+		}));
+
+		ars.pipe(aws);
+		return;
+	}
+
+	let fileData = RocketChatFile.dataURIParse(dataURI);
+	let image = fileData.image;
+	contentType = fileData.contentType;
+
+	let rs = RocketChatFile.bufferToStream(new Buffer(image, 'base64'));
+	RocketChatFileAvatarInstance.deleteFile(encodeURIComponent(`${user.username}.jpg`));
+	let ws = RocketChatFileAvatarInstance.createWriteStream(encodeURIComponent(`${user.username}.jpg`), contentType);
+	ws.on('end', Meteor.bindEnvironment(function() {
+		Meteor.setTimeout(function() {
+			RocketChat.models.Users.setAvatarOrigin(user._id, service);
+			RocketChat.Notifications.notifyAll('updateAvatar', {username: user.username});
+		}, 500);
+	}));
+	rs.pipe(ws);
+};
diff --git a/packages/rocketchat-lib/server/functions/unarchiveRoom.js b/packages/rocketchat-lib/server/functions/unarchiveRoom.js
new file mode 100644
index 0000000000000000000000000000000000000000..3884e06c4041967dbc58c07d91dbaa488258ae12
--- /dev/null
+++ b/packages/rocketchat-lib/server/functions/unarchiveRoom.js
@@ -0,0 +1,4 @@
+RocketChat.unarchiveRoom = function(rid) {
+	RocketChat.models.Rooms.unarchiveById(rid);
+	RocketChat.models.Subscriptions.unarchiveByRoomId(rid);
+};
diff --git a/packages/rocketchat-lib/server/functions/updateMessage.js b/packages/rocketchat-lib/server/functions/updateMessage.js
new file mode 100644
index 0000000000000000000000000000000000000000..38141a03d61cb630cddc892f895b3a2811452c4c
--- /dev/null
+++ b/packages/rocketchat-lib/server/functions/updateMessage.js
@@ -0,0 +1,30 @@
+RocketChat.updateMessage = function(message, user) {
+	// If we keep history of edits, insert a new message to store history information
+	if (RocketChat.settings.get('Message_KeepHistory')) {
+		RocketChat.models.Messages.cloneAndSaveAsHistoryById(message._id);
+	}
+
+	message.editedAt = new Date();
+	message.editedBy = {
+		_id: user._id,
+		username: user.username
+	};
+
+	let urls = message.msg.match(/([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g);
+	if (urls) {
+		message.urls = urls.map((url) => { return { url: url }; });
+	}
+
+	message = RocketChat.callbacks.run('beforeSaveMessage', message);
+
+	let tempid = message._id;
+	delete message._id;
+
+	RocketChat.models.Messages.update({ _id: tempid }, { $set: message });
+
+	let room = RocketChat.models.Rooms.findOneById(message.rid);
+
+	Meteor.defer(function() {
+		RocketChat.callbacks.run('afterSaveMessage', RocketChat.models.Messages.findOneById(tempid), room);
+	});
+};
diff --git a/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js b/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js
index e1e126f91728f5b431acbee17a9c9ea0a4ad0585..641d6b87198d14c50b4be304e872d919d0c26a98 100644
--- a/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js
+++ b/packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js
@@ -73,7 +73,7 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) {
 
 	// Update all other subscriptions to alert their owners but witout incrementing
 	// the unread counter, as it is only for mentions and direct messages
-	RocketChat.models.Subscriptions.setAlertForRoomIdExcludingUserId(message.rid, message.u._id, true);
+	RocketChat.models.Subscriptions.setAlertForRoomIdExcludingUserId(message.rid, message.u._id);
 
 	return message;
 
diff --git a/packages/rocketchat-lib/server/lib/sendEmailOnMessage.js b/packages/rocketchat-lib/server/lib/sendEmailOnMessage.js
index c80b88e6f9e461db777db377814d83eeaa3c3124..8ca68065ad7a70d2f797d2201b7570612089c91f 100644
--- a/packages/rocketchat-lib/server/lib/sendEmailOnMessage.js
+++ b/packages/rocketchat-lib/server/lib/sendEmailOnMessage.js
@@ -36,17 +36,13 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) {
 		var path = Meteor.absoluteUrl(roomPath ? roomPath.replace(/^\//, '') : '');
 		var style = [
 			'color: #fff;',
-			'padding: .5em;',
+			'padding: 9px 12px;',
+			'border-radius: 4px;',
 			'background-color: #04436a;',
-			'display: block;',
-			'width: 10em;',
-			'text-align: center;',
-			'text-decoration: none;',
-			'margin: auto;',
-			'margin-bottom: 8px;'
+			'text-decoration: none;'
 		].join(' ');
 		var message = TAPi18n.__('Offline_Link_Message');
-		return `<a style="${ style }" href="${ path }">${ message }</a>`;
+		return `<p style="text-align:center;margin-bottom:8px;"><a style="${ style }" href="${ path }">${ message }</a>`;
 	};
 
 	var divisorMessage = '<hr style="margin: 20px auto; border: none; border-bottom: 1px solid #dddddd;">';
diff --git a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js
index b5e60e337d4235ab8d7719a44ea280a221373bfb..0f931d1c4461d5bb90ffd2b50706cf2429461437 100644
--- a/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js
+++ b/packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js
@@ -6,6 +6,10 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) {
 		return message;
 	}
 
+	if (message.ts && Math.abs(moment(message.ts).diff()) > 60000) {
+		return message;
+	}
+
 	var user = RocketChat.models.Users.findOneById(message.u._id);
 
 	/*
diff --git a/packages/rocketchat-lib/server/methods/addOAuthService.coffee b/packages/rocketchat-lib/server/methods/addOAuthService.coffee
index 19606e836c58e9c651db3d53692f51a66fea397b..b3ff589bc14a925714f18e5b3376cb6709b43267 100644
--- a/packages/rocketchat-lib/server/methods/addOAuthService.coffee
+++ b/packages/rocketchat-lib/server/methods/addOAuthService.coffee
@@ -1,5 +1,8 @@
 Meteor.methods
 	addOAuthService: (name) ->
+
+		check name, String
+
 		if not Meteor.userId()
 			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addOAuthService' })
 
diff --git a/server/methods/addUserToRoom.coffee b/packages/rocketchat-lib/server/methods/addUserToRoom.coffee
similarity index 60%
rename from server/methods/addUserToRoom.coffee
rename to packages/rocketchat-lib/server/methods/addUserToRoom.coffee
index 41a086d4beaf78e8c6a01f3546b56d0aa3c274d2..47287b4d05c294f7dd0ebe636c8410a9ff178a0c 100644
--- a/server/methods/addUserToRoom.coffee
+++ b/packages/rocketchat-lib/server/methods/addUserToRoom.coffee
@@ -3,7 +3,6 @@ Meteor.methods
 		if not Meteor.userId()
 			throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'addUserToRoom' }
 
-		fromId = Meteor.userId()
 		unless Match.test data?.rid, String
 			throw new Meteor.Error 'error-invalid-room', 'Invalid room', { method: 'addUserToRoom' }
 
@@ -15,37 +14,15 @@ Meteor.methods
 		if room.usernames.indexOf(Meteor.user().username) is -1
 			throw new Meteor.Error 'error-not-allowed', 'Not allowed', { method: 'addUserToRoom' }
 
-		# if room.username isnt Meteor.user().username and room.t is 'c'
+		fromId = Meteor.userId()
 		if not RocketChat.authz.hasPermission(fromId, 'add-user-to-room', room._id)
 			throw new Meteor.Error 'error-not-allowed', 'Not allowed', { method: 'addUserToRoom' }
 
 		if room.t is 'd'
 			throw new Meteor.Error 'error-cant-invite-for-direct-room', 'Can\'t invite user to direct rooms', { method: 'addUserToRoom' }
 
-		# verify if user is already in room
-		if room.usernames.indexOf(data.username) isnt -1
-			return
 
 		newUser = RocketChat.models.Users.findOneByUsername data.username
-
-
-		muted = room.ro and not RocketChat.authz.hasPermission(newUser._id, 'post-read-only')
-
-		RocketChat.models.Rooms.addUsernameById data.rid, data.username, muted
-
-		now = new Date()
-
-		RocketChat.models.Subscriptions.createWithRoomAndUser room, newUser,
-			ts: now
-			open: true
-			alert: true
-			unread: 1
-
-		fromUser = RocketChat.models.Users.findOneById fromId
-		RocketChat.models.Messages.createUserAddedWithRoomIdAndUser data.rid, newUser,
-			ts: now
-			u:
-				_id: fromUser._id
-				username: fromUser.username
+		RocketChat.addUserToRoom(data.rid, newUser, Meteor.user());
 
 		return true
diff --git a/server/methods/archiveRoom.coffee b/packages/rocketchat-lib/server/methods/archiveRoom.coffee
similarity index 63%
rename from server/methods/archiveRoom.coffee
rename to packages/rocketchat-lib/server/methods/archiveRoom.coffee
index 6f8030e2df203b38e6ccf1fef92389164fb23a34..9f7d75cfce1ade85c3339d09aade1060cc48670d 100644
--- a/server/methods/archiveRoom.coffee
+++ b/packages/rocketchat-lib/server/methods/archiveRoom.coffee
@@ -11,11 +11,4 @@ Meteor.methods
 		unless RocketChat.authz.hasPermission(Meteor.userId(), 'archive-room', room._id)
 			throw new Meteor.Error 'error-not-authorized', 'Not authorized', { method: 'archiveRoom' }
 
-		RocketChat.models.Rooms.archiveById rid
-
-		for username in room.usernames
-			member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1 }})
-			if not member?
-				continue
-
-			RocketChat.models.Subscriptions.archiveByRoomIdAndUserId rid, member._id
+		RocketChat.archiveRoom(rid)
diff --git a/packages/rocketchat-lib/server/methods/checkRegistrationSecretURL.coffee b/packages/rocketchat-lib/server/methods/checkRegistrationSecretURL.coffee
index 592b85470995143fba4caaf3ba22e11de7ec55f9..e2b376ebcfe127b7b796589ca336152622fb3d8d 100644
--- a/packages/rocketchat-lib/server/methods/checkRegistrationSecretURL.coffee
+++ b/packages/rocketchat-lib/server/methods/checkRegistrationSecretURL.coffee
@@ -1,3 +1,6 @@
 Meteor.methods
 	checkRegistrationSecretURL: (hash) ->
+
+		check hash, String
+
 		return hash is RocketChat.settings.get 'Accounts_RegistrationForm_SecretURL'
diff --git a/packages/rocketchat-lib/server/methods/createChannel.coffee b/packages/rocketchat-lib/server/methods/createChannel.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..3fbe6bd3174f821d3da06ddbfe02eef2aa155ea0
--- /dev/null
+++ b/packages/rocketchat-lib/server/methods/createChannel.coffee
@@ -0,0 +1,9 @@
+Meteor.methods
+	createChannel: (name, members, readOnly) ->
+		if not Meteor.userId()
+			throw new Meteor.Error 'error-invalid-user', "Invalid user", { method: 'createChannel' }
+
+		if RocketChat.authz.hasPermission(Meteor.userId(), 'create-c') isnt true
+			throw new Meteor.Error 'error-not-allowed', "Not allowed", { method: 'createChannel' }
+
+		return RocketChat.createRoom('c', name, Meteor.user()?.username, members, readOnly);
diff --git a/packages/rocketchat-lib/server/methods/createPrivateGroup.coffee b/packages/rocketchat-lib/server/methods/createPrivateGroup.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..e90eec5033381b61a7942bc41021674d4fe92684
--- /dev/null
+++ b/packages/rocketchat-lib/server/methods/createPrivateGroup.coffee
@@ -0,0 +1,9 @@
+Meteor.methods
+	createPrivateGroup: (name, members) ->
+		if not Meteor.userId()
+			throw new Meteor.Error 'error-invalid-user', "Invalid user", { method: 'createPrivateGroup' }
+
+		unless RocketChat.authz.hasPermission(Meteor.userId(), 'create-p')
+			throw new Meteor.Error 'error-not-allowed', "Not allowed", { method: 'createPrivateGroup' }
+
+		return RocketChat.createRoom('p', name, Meteor.user()?.username, members);
diff --git a/server/methods/deleteMessage.coffee b/packages/rocketchat-lib/server/methods/deleteMessage.coffee
similarity index 60%
rename from server/methods/deleteMessage.coffee
rename to packages/rocketchat-lib/server/methods/deleteMessage.coffee
index ac87740869e153dfd5e4eb3f2e070a7cf3ea683d..fa7ddfe58e1e7e9bce8f756e8ea5c90e5ab54392 100644
--- a/server/methods/deleteMessage.coffee
+++ b/packages/rocketchat-lib/server/methods/deleteMessage.coffee
@@ -22,27 +22,4 @@ Meteor.methods
 			if currentTsDiff > blockDeleteInMinutes
 				throw new Meteor.Error 'error-message-deleting-blocked', 'Message deleting is blocked', { method: 'deleteMessage' }
 
-
-		keepHistory = RocketChat.settings.get 'Message_KeepHistory'
-		showDeletedStatus = RocketChat.settings.get 'Message_ShowDeletedStatus'
-
-		if keepHistory
-			if showDeletedStatus
-				RocketChat.models.Messages.cloneAndSaveAsHistoryById originalMessage._id
-			else
-				RocketChat.models.Messages.setHiddenById originalMessage._id, true
-
-			if originalMessage.file?._id?
-				RocketChat.models.Uploads.update originalMessage.file._id, {$set: {_hidden: true}}
-
-		else
-			if not showDeletedStatus
-				RocketChat.models.Messages.removeById originalMessage._id
-
-			if originalMessage.file?._id?
-				FileUpload.delete(originalMessage.file._id)
-
-		if showDeletedStatus
-			RocketChat.models.Messages.setAsDeletedById originalMessage._id
-		else
-			RocketChat.Notifications.notifyRoom originalMessage.rid, 'deleteMessage', {_id: originalMessage._id}
+		RocketChat.deleteMessage(originalMessage, Meteor.user());
diff --git a/packages/rocketchat-lib/server/methods/deleteUserOwnAccount.js b/packages/rocketchat-lib/server/methods/deleteUserOwnAccount.js
index ee4d15239cfd572124e85b09f8b55590e771d755..ed7f361c4d3b3192bfe53ed59e80fcd23a9ed8f4 100644
--- a/packages/rocketchat-lib/server/methods/deleteUserOwnAccount.js
+++ b/packages/rocketchat-lib/server/methods/deleteUserOwnAccount.js
@@ -1,5 +1,8 @@
 Meteor.methods({
 	deleteUserOwnAccount: function(password) {
+
+		check(password, String);
+
 		if (!Meteor.userId()) {
 			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'deleteUserOwnAccount' });
 		}
diff --git a/packages/rocketchat-lib/server/methods/filterATAllTag.js b/packages/rocketchat-lib/server/methods/filterATAllTag.js
index 38e4f05aea12b1ea533fddd5dc0e9769ad681e45..4691004b5f3b9432d9ff08421cd24a517b59c509 100644
--- a/packages/rocketchat-lib/server/methods/filterATAllTag.js
+++ b/packages/rocketchat-lib/server/methods/filterATAllTag.js
@@ -1,5 +1,4 @@
 RocketChat.callbacks.add('beforeSaveMessage', function(message) {
-
 	// Test if the message mentions include @all.
 	if (message.mentions != null &&
 		_.pluck(message.mentions, '_id').some((item) => item === 'all')) {
diff --git a/packages/rocketchat-lib/server/methods/getRoomRoles.js b/packages/rocketchat-lib/server/methods/getRoomRoles.js
index 75e953a890552249516b19f684b14b248ddefe4a..18ccaed6dbefd517b1015ddff0bff699905cca2c 100644
--- a/packages/rocketchat-lib/server/methods/getRoomRoles.js
+++ b/packages/rocketchat-lib/server/methods/getRoomRoles.js
@@ -1,5 +1,8 @@
 Meteor.methods({
 	getRoomRoles(rid) {
+
+		check(rid, String);
+
 		if (!Meteor.userId()) {
 			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getRoomRoles' });
 		}
diff --git a/packages/rocketchat-lib/server/methods/getUserRoles.js b/packages/rocketchat-lib/server/methods/getUserRoles.js
index cff85925e8768e7e000972ffeb3df649bcb6176b..f51e7c9e75885e6c693d5877dec88f7d150687df 100644
--- a/packages/rocketchat-lib/server/methods/getUserRoles.js
+++ b/packages/rocketchat-lib/server/methods/getUserRoles.js
@@ -1,5 +1,6 @@
 Meteor.methods({
 	getUserRoles() {
+
 		if (!Meteor.userId()) {
 			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getUserRoles' });
 		}
diff --git a/packages/rocketchat-lib/server/methods/insertOrUpdateUser.coffee b/packages/rocketchat-lib/server/methods/insertOrUpdateUser.coffee
index 2cb02948589ab9b7580b0eca451f1d2be6797cb4..221c2f3c824ddb9f495bdf3d7556624d37c8704d 100644
--- a/packages/rocketchat-lib/server/methods/insertOrUpdateUser.coffee
+++ b/packages/rocketchat-lib/server/methods/insertOrUpdateUser.coffee
@@ -1,5 +1,8 @@
 Meteor.methods
 	insertOrUpdateUser: (userData) ->
+
+		check userData, Object
+
 		if not Meteor.userId()
 			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'insertOrUpdateUser' })
 
@@ -97,11 +100,6 @@ Meteor.methods
 
 			return _id
 		else
-			# prevent removing admin role of last admin
-			adminCount = Meteor.users.find({ roles: { $in: ['admin'] } }).count()
-			if adminCount is 1 and userData.role isnt 'admin'
-				throw new Meteor.Error 'error-action-not-allowed', 'Leaving the app without admins is not allowed', { method: 'insertOrUpdateUser', action: 'Remove_last_admin' }
-
 			#update user
 			updateUser = $set: {}
 
diff --git a/packages/rocketchat-lib/server/methods/joinDefaultChannels.coffee b/packages/rocketchat-lib/server/methods/joinDefaultChannels.coffee
index 120eef213c6781af8c4e494d58c5743031f78671..fd748483a86d147fa835ad56ddeb30fd0b1a2ed0 100644
--- a/packages/rocketchat-lib/server/methods/joinDefaultChannels.coffee
+++ b/packages/rocketchat-lib/server/methods/joinDefaultChannels.coffee
@@ -1,30 +1,10 @@
 Meteor.methods
 	joinDefaultChannels: (silenced) ->
-		if not Meteor.userId()
-			throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'joinDefaultChannels' })
-
-		this.unblock()
-
-		user = Meteor.user()
-
-		RocketChat.callbacks.run 'beforeJoinDefaultChannels', user
 
-		defaultRooms = RocketChat.models.Rooms.findByDefaultAndTypes(true, ['c', 'p'], {fields: {usernames: 0}}).fetch()
+		check silenced, Match.Optional(Boolean)
 
-		defaultRooms.forEach (room) ->
-
-			# put user in default rooms
-			RocketChat.models.Rooms.addUsernameById room._id, user.username
-
-			if not RocketChat.models.Subscriptions.findOneByRoomIdAndUserId(room._id, user._id)?
-
-				# Add a subscription to this user
-				RocketChat.models.Subscriptions.createWithRoomAndUser room, user,
-					ts: new Date()
-					open: true
-					alert: true
-					unread: 1
+		if not Meteor.userId()
+			throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'joinDefaultChannels' })
 
-				# Insert user joined message
-				if not silenced
-					RocketChat.models.Messages.createUserJoinWithRoomIdAndUser room._id, user
+		this.unblock();
+		RocketChat.addUserToDefaultChannels(Meteor.user(), silenced);
diff --git a/packages/rocketchat-lib/server/methods/joinRoom.coffee b/packages/rocketchat-lib/server/methods/joinRoom.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..095e23dd22c5d6d0aef06c7755cd75211cb71220
--- /dev/null
+++ b/packages/rocketchat-lib/server/methods/joinRoom.coffee
@@ -0,0 +1,17 @@
+Meteor.methods
+	joinRoom: (rid, code) ->
+		if not Meteor.userId()
+			throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'joinRoom' }
+
+		room = RocketChat.models.Rooms.findOneById rid
+
+		if not room?
+			throw new Meteor.Error 'error-invalid-room', 'Invalid room', { method: 'joinRoom' }
+
+		if room.t isnt 'c' or RocketChat.authz.hasPermission(Meteor.userId(), 'view-c-room') isnt true
+			throw new Meteor.Error 'error-not-allowed', 'Not allowed', { method: 'joinRoom' }
+
+		if room.joinCodeRequired is true and code isnt room.joinCode
+			throw new Meteor.Error 'error-code-invalid', 'Invalid Code', { method: 'joinRoom' }
+
+		RocketChat.addUserToRoom(rid, Meteor.user())
diff --git a/server/methods/leaveRoom.coffee b/packages/rocketchat-lib/server/methods/leaveRoom.coffee
similarity index 56%
rename from server/methods/leaveRoom.coffee
rename to packages/rocketchat-lib/server/methods/leaveRoom.coffee
index c7931f0c7587262cfeab5e4b5c150e269c660c08..f00ab1de6a55923c4384b2564b94acf872419ec7 100644
--- a/server/methods/leaveRoom.coffee
+++ b/packages/rocketchat-lib/server/methods/leaveRoom.coffee
@@ -15,19 +15,4 @@ Meteor.methods
 			if numOwners is 1
 				throw new Meteor.Error 'error-you-are-last-owner', 'You are the last owner. Please set new owner before leaving the room.', { method: 'leaveRoom' }
 
-		RocketChat.callbacks.run 'beforeLeaveRoom', user, room
-
-		RocketChat.models.Rooms.removeUsernameById rid, user.username
-
-		if room.usernames.indexOf(user.username) isnt -1
-			removedUser = user
-			RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser rid, removedUser
-
-		if room.t is 'l'
-			RocketChat.models.Messages.createCommandWithRoomIdAndUser 'survey', rid, user
-
-		RocketChat.models.Subscriptions.removeByRoomIdAndUserId rid, Meteor.userId()
-
-		Meteor.defer ->
-
-			RocketChat.callbacks.run 'afterLeaveRoom', user, room
+		RocketChat.removeUserFromRoom(rid, Meteor.user());
diff --git a/packages/rocketchat-lib/server/methods/removeOAuthService.coffee b/packages/rocketchat-lib/server/methods/removeOAuthService.coffee
index f13b9abdd7dc7faf0afea6890fd9367a4b62a5a7..689609246ecfd52809a0220f3ad14755b9de0a92 100644
--- a/packages/rocketchat-lib/server/methods/removeOAuthService.coffee
+++ b/packages/rocketchat-lib/server/methods/removeOAuthService.coffee
@@ -1,5 +1,8 @@
 Meteor.methods
 	removeOAuthService: (name) ->
+
+		check name, String
+
 		if not Meteor.userId()
 			throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'removeOAuthService' })
 
diff --git a/packages/rocketchat-lib/server/methods/robotMethods.coffee b/packages/rocketchat-lib/server/methods/robotMethods.coffee
index 22cf5ba98560ace324d1aec67ba42e58e699b310..593a915416ab4baa172ef4b0e4ec1cdc72beac74 100644
--- a/packages/rocketchat-lib/server/methods/robotMethods.coffee
+++ b/packages/rocketchat-lib/server/methods/robotMethods.coffee
@@ -1,5 +1,9 @@
 Meteor.methods
 	'robot.modelCall': (model, method, args) ->
+
+		check model, String
+		check method, String
+
 		unless Meteor.userId()
 			throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'robot.modelCall' }
 
diff --git a/packages/rocketchat-lib/server/methods/saveSetting.coffee b/packages/rocketchat-lib/server/methods/saveSetting.coffee
deleted file mode 100644
index cbec9d4d2d1d1f6da91d6dd70507ad24d80bd34e..0000000000000000000000000000000000000000
--- a/packages/rocketchat-lib/server/methods/saveSetting.coffee
+++ /dev/null
@@ -1,11 +0,0 @@
-Meteor.methods
-	saveSetting: (_id, value) ->
-		if Meteor.userId()?
-			user = Meteor.users.findOne Meteor.userId()
-
-		unless RocketChat.authz.hasPermission(Meteor.userId(), 'edit-privileged-setting') is true
-			throw new Meteor.Error 'error-action-not-allowed', 'Editing settings is not allowed', { method: 'saveSetting' }
-
-		# console.log "saveSetting -> ".green, _id, value
-		RocketChat.settings.updateById _id, value
-		return true
diff --git a/packages/rocketchat-lib/server/methods/saveSetting.js b/packages/rocketchat-lib/server/methods/saveSetting.js
new file mode 100644
index 0000000000000000000000000000000000000000..d5bb8224eb30e441f6e12bd5f0bdefe6565d13ff
--- /dev/null
+++ b/packages/rocketchat-lib/server/methods/saveSetting.js
@@ -0,0 +1,36 @@
+Meteor.methods({
+	saveSetting: function(_id, value) {
+		if (Meteor.userId() === null) {
+			throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', {
+				method: 'saveSetting'
+			});
+		}
+
+		if (!RocketChat.authz.hasPermission(Meteor.userId(), 'edit-privileged-setting')) {
+			throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', {
+				method: 'saveSetting'
+			});
+		}
+
+		//Verify the _id passed in is a string.
+		check(_id, String);
+
+		const setting = RocketChat.models.Settings.findOneById(_id);
+
+		//Verify the value is what it should be
+		switch (setting.type) {
+			case 'boolean':
+				check(value, Boolean);
+				break;
+			case 'int':
+				check(value, Number);
+				break;
+			default:
+				check(value, String);
+				break;
+		}
+
+		RocketChat.settings.updateById(_id, value);
+		return true;
+	}
+});
diff --git a/packages/rocketchat-lib/server/methods/sendInvitationEmail.coffee b/packages/rocketchat-lib/server/methods/sendInvitationEmail.coffee
index 9be056c3b2867f5b24870760e96da50eab8fe610..f9d8080a619258631f04dfb792b3b7ef9cd3106b 100644
--- a/packages/rocketchat-lib/server/methods/sendInvitationEmail.coffee
+++ b/packages/rocketchat-lib/server/methods/sendInvitationEmail.coffee
@@ -1,5 +1,8 @@
 Meteor.methods
 	sendInvitationEmail: (emails) ->
+
+		check emails, [String]
+
 		if not Meteor.userId()
 			throw new Meteor.Error 'error-invalid-user', "Invalid user", { method: 'sendInvitationEmail' }
 
diff --git a/packages/rocketchat-lib/server/methods/sendMessage.coffee b/packages/rocketchat-lib/server/methods/sendMessage.coffee
index f9d5c56043bd834014ca8c469b2089fb87bc1aee..a2d52af246a806c71c7847d8393117a700999a86 100644
--- a/packages/rocketchat-lib/server/methods/sendMessage.coffee
+++ b/packages/rocketchat-lib/server/methods/sendMessage.coffee
@@ -1,6 +1,8 @@
 Meteor.methods
 	sendMessage: (message) ->
 
+		check message, Object
+
 		if message.ts
 			tsDiff = Math.abs(moment(message.ts).diff())
 			if tsDiff > 60000
diff --git a/packages/rocketchat-lib/server/methods/setAdminStatus.coffee b/packages/rocketchat-lib/server/methods/setAdminStatus.coffee
index 23df6a18b8a6a306ce8538f33c1ec27d0752d13d..ca1439aa92434a5375e207e5727499627f9c9840 100644
--- a/packages/rocketchat-lib/server/methods/setAdminStatus.coffee
+++ b/packages/rocketchat-lib/server/methods/setAdminStatus.coffee
@@ -1,5 +1,9 @@
 Meteor.methods
 	setAdminStatus: (userId, admin) ->
+
+		check userId, String
+		check admin, Match.Optional(Boolean)
+
 		if not Meteor.userId()
 			throw new Meteor.Error 'error-invalid-user', "Invalid user", { method: 'setAdminStatus' }
 
diff --git a/packages/rocketchat-lib/server/methods/setEmail.js b/packages/rocketchat-lib/server/methods/setEmail.js
index c7d456b52e388af30a0608fa486f4835966aad92..e71db53808d4ea17fafe90f117924826d05d3fe7 100644
--- a/packages/rocketchat-lib/server/methods/setEmail.js
+++ b/packages/rocketchat-lib/server/methods/setEmail.js
@@ -1,5 +1,8 @@
 Meteor.methods({
 	setEmail: function(email) {
+
+		check (email, String);
+
 		if (!Meteor.userId()) {
 			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setEmail' });
 		}
diff --git a/packages/rocketchat-lib/server/methods/setRealName.coffee b/packages/rocketchat-lib/server/methods/setRealName.coffee
index f2aa510b4fe29ff8c53e8bd3be7c122af1ed7b2f..072a1f4dfa027a243432d5bfc0235f12f1c24bdd 100644
--- a/packages/rocketchat-lib/server/methods/setRealName.coffee
+++ b/packages/rocketchat-lib/server/methods/setRealName.coffee
@@ -1,5 +1,8 @@
 Meteor.methods
 	setRealName: (name) ->
+
+		check name, String
+
 		if not Meteor.userId()
 			throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'setRealName' })
 
diff --git a/packages/rocketchat-lib/server/methods/setUsername.coffee b/packages/rocketchat-lib/server/methods/setUsername.coffee
index fe319d1b8bf6928bf3a14d945893e2a92dd7e818..4031056c91d13b13eaa86b1babc96a1e5e2d109e 100644
--- a/packages/rocketchat-lib/server/methods/setUsername.coffee
+++ b/packages/rocketchat-lib/server/methods/setUsername.coffee
@@ -1,5 +1,8 @@
 Meteor.methods
 	setUsername: (username) ->
+
+		check username, String
+
 		if not Meteor.userId()
 			throw new Meteor.Error('error-invalid-user', "Invalid user", { method: 'setUsername' })
 
diff --git a/server/methods/unarchiveRoom.coffee b/packages/rocketchat-lib/server/methods/unarchiveRoom.coffee
similarity index 63%
rename from server/methods/unarchiveRoom.coffee
rename to packages/rocketchat-lib/server/methods/unarchiveRoom.coffee
index 8bf78dea7b1249300030457e9b66c71de74ff0c1..699398bf06e03229ba6cda34ca41f6c909bdfb2e 100644
--- a/server/methods/unarchiveRoom.coffee
+++ b/packages/rocketchat-lib/server/methods/unarchiveRoom.coffee
@@ -11,11 +11,4 @@ Meteor.methods
 		unless RocketChat.authz.hasPermission(Meteor.userId(), 'unarchive-room', room._id)
 			throw new Meteor.Error 'error-not-authorized', 'Not authorized', { method: 'unarchiveRoom' }
 
-		RocketChat.models.Rooms.unarchiveById rid
-
-		for username in room.usernames
-			member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1 }})
-			if not member?
-				continue
-
-			RocketChat.models.Subscriptions.unarchiveByRoomIdAndUserId rid, member._id
+		RocketChat.unarchiveRoom(rid);
diff --git a/server/methods/updateMessage.coffee b/packages/rocketchat-lib/server/methods/updateMessage.coffee
similarity index 56%
rename from server/methods/updateMessage.coffee
rename to packages/rocketchat-lib/server/methods/updateMessage.coffee
index 162dc26972de9be7233fe75c6966c87b5b3adbef..1e9d27e656965983481a2b421ba8688759dc9734 100644
--- a/server/methods/updateMessage.coffee
+++ b/packages/rocketchat-lib/server/methods/updateMessage.coffee
@@ -24,29 +24,4 @@ Meteor.methods
 			if currentTsDiff > blockEditInMinutes
 				throw new Meteor.Error 'error-message-editing-blocked', 'Message editing is blocked', { method: 'updateMessage' }
 
-		# If we keep history of edits, insert a new message to store history information
-		if RocketChat.settings.get 'Message_KeepHistory'
-			RocketChat.models.Messages.cloneAndSaveAsHistoryById originalMessage._id
-
-		message.editedAt = new Date()
-		message.editedBy =
-			_id: Meteor.userId()
-			username: me.username
-
-		if urls = message.msg.match /([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g
-			message.urls = urls.map (url) -> url: url
-
-		message = RocketChat.callbacks.run 'beforeSaveMessage', message
-
-		tempid = message._id
-		delete message._id
-
-		RocketChat.models.Messages.update
-			_id: tempid
-		,
-			$set: message
-
-		room = RocketChat.models.Rooms.findOneById message.rid
-
-		Meteor.defer ->
-			RocketChat.callbacks.run 'afterSaveMessage', RocketChat.models.Messages.findOneById(tempid), room
+		RocketChat.updateMessage(message, Meteor.user());
diff --git a/packages/rocketchat-lib/server/models/Messages.coffee b/packages/rocketchat-lib/server/models/Messages.coffee
index 12a26916e8a305c0406ea3126fa19fa9cd3130ba..2641b093455f85655f8b80c42f0436b610551f98 100644
--- a/packages/rocketchat-lib/server/models/Messages.coffee
+++ b/packages/rocketchat-lib/server/models/Messages.coffee
@@ -152,8 +152,7 @@ RocketChat.models.Messages = new class extends RocketChat.models._Base
 
 		return @update query, update
 
-	setAsDeletedById: (_id) ->
-		me = RocketChat.models.Users.findOneById Meteor.userId()
+	setAsDeletedByIdAndUser: (_id, user) ->
 		query =
 			_id: _id
 
@@ -166,8 +165,8 @@ RocketChat.models.Messages = new class extends RocketChat.models._Base
 				attachments: []
 				editedAt: new Date()
 				editedBy:
-					_id: Meteor.userId()
-					username: me.username
+					_id: user._id
+					username: user.username
 
 		return @update query, update
 
diff --git a/packages/rocketchat-lib/server/models/Rooms.coffee b/packages/rocketchat-lib/server/models/Rooms.coffee
index 752e7388eab979f2cf484e0dfa1d692493a1cbe6..97c9be55d85c258c1f1039c1f47bd9b53df1237b 100644
--- a/packages/rocketchat-lib/server/models/Rooms.coffee
+++ b/packages/rocketchat-lib/server/models/Rooms.coffee
@@ -366,6 +366,24 @@ RocketChat.models.Rooms = new class extends RocketChat.models._Base
 
 		return @update query, update, { multi: true }
 
+	setJoinCodeById: (_id, joinCode) ->
+		query =
+			_id: _id
+
+		if joinCode?.trim() isnt ''
+			update =
+				$set:
+					joinCodeRequired: true
+					joinCode: joinCode
+		else
+			update =
+				$set:
+					joinCodeRequired: false
+				$unset:
+					joinCode: 1
+
+		return @update query, update
+
 	setUserById: (_id, user) ->
 		query =
 			_id: _id
diff --git a/packages/rocketchat-lib/server/models/Subscriptions.coffee b/packages/rocketchat-lib/server/models/Subscriptions.coffee
index b40095e3faf18968ab550704e61e9707373da69f..c5b30c6139d4448793af5f89d9b3881f3e898a24 100644
--- a/packages/rocketchat-lib/server/models/Subscriptions.coffee
+++ b/packages/rocketchat-lib/server/models/Subscriptions.coffee
@@ -87,10 +87,9 @@ RocketChat.models.Subscriptions = new class extends RocketChat.models._Base
 		return @find query
 
 	# UPDATE
-	archiveByRoomIdAndUserId: (roomId, userId) ->
+	archiveByRoomId: (roomId) ->
 		query =
 			rid: roomId
-			'u._id': userId
 
 		update =
 			$set:
@@ -100,10 +99,9 @@ RocketChat.models.Subscriptions = new class extends RocketChat.models._Base
 
 		return @update query, update
 
-	unarchiveByRoomIdAndUserId: (roomId, userId) ->
+	unarchiveByRoomId: (roomId) ->
 		query =
 			rid: roomId
-			'u._id': userId
 
 		update =
 			$set:
@@ -262,17 +260,19 @@ RocketChat.models.Subscriptions = new class extends RocketChat.models._Base
 
 		return @update query, update, { multi: true }
 
-	setAlertForRoomIdExcludingUserId: (roomId, userId, alert=true) ->
+	setAlertForRoomIdExcludingUserId: (roomId, userId) ->
 		query =
 			rid: roomId
-			alert:
-				$ne: alert
 			'u._id':
 				$ne: userId
+			$or: [
+				{ alert: { $ne: true } }
+				{ open: { $ne: true } }
+			]
 
 		update =
 			$set:
-				alert: alert
+				alert: true
 				open: true
 
 		return @update query, update, { multi: true }
diff --git a/packages/rocketchat-lib/server/models/Users.coffee b/packages/rocketchat-lib/server/models/Users.coffee
index 9f8dc6cac6c069ffc113db6f2cf655fb9aa15df7..90b583664b28cb33fa7301819e4b953656d86b65 100644
--- a/packages/rocketchat-lib/server/models/Users.coffee
+++ b/packages/rocketchat-lib/server/models/Users.coffee
@@ -371,15 +371,16 @@ RocketChat.models.Users = new class extends RocketChat.models._Base
 	- he is not online
 	- has a verified email
 	- has not disabled email notifications
+	- `active` is equal to true (false means they were deactivated and can't login)
 	###
 	getUsersToSendOfflineEmail: (usersIds) ->
 		query =
 			_id:
 				$in: usersIds
+			active: true
 			status: 'offline'
 			statusConnection:
 				$ne: 'online'
 			'emails.verified': true
 
 		return @find query, { fields: { name: 1, username: 1, emails: 1, 'settings.preferences.emailNotificationMode': 1 } }
-
diff --git a/packages/rocketchat-lib/server/models/_Base.js b/packages/rocketchat-lib/server/models/_Base.js
index 89dad0e62f179d4731907ceed8480b9ed2eccfbf..d0f3de53f5f9d70d13a9feab0cc31dfd4c083221 100644
--- a/packages/rocketchat-lib/server/models/_Base.js
+++ b/packages/rocketchat-lib/server/models/_Base.js
@@ -74,10 +74,10 @@ class ModelsBase extends EventEmitter {
 			}
 		}
 
-		query = { _id: { $in: _.pluck(ids, '_id') } };
 		const result = this.model.update(query, update, options);
-		this.emit('update', query, update);
-		this.emit('change', 'update', query, update);
+		const idQuery = { _id: { $in: _.pluck(ids, '_id') } };
+		this.emit('update', idQuery, update);
+		this.emit('change', 'update', idQuery, update);
 		return result;
 	}
 
diff --git a/packages/rocketchat-lib/server/startup/settings.coffee b/packages/rocketchat-lib/server/startup/settings.coffee
index 121c6fccd16c8f8355b0e3972b628cc5888503da..941f3c673726f085ffb54ee691f07cee6f61f737 100644
--- a/packages/rocketchat-lib/server/startup/settings.coffee
+++ b/packages/rocketchat-lib/server/startup/settings.coffee
@@ -196,12 +196,12 @@ RocketChat.settings.addGroup 'Meta', ->
 
 
 RocketChat.settings.addGroup 'Push', ->
-	@add 'Push_debug', false, { type: 'boolean', public: true }
 	@add 'Push_enable', true, { type: 'boolean', public: true }
-	@add 'Push_enable_gateway', true, { type: 'boolean' }
-	@add 'Push_gateway', 'https://rocket.chat', { type: 'string' }
-	@add 'Push_production', true, { type: 'boolean', public: true }
-	@add 'Push_test_push', 'push_test', { type: 'action', actionText: 'Send_a_test_push_to_my_user' }
+	@add 'Push_debug', false, { type: 'boolean', public: true, enableQuery: { _id: 'Push_enable', value: true } }
+	@add 'Push_enable_gateway', true, { type: 'boolean', enableQuery: { _id: 'Push_enable', value: true } }
+	@add 'Push_gateway', 'https://gateway.rocket.chat', { type: 'string', enableQuery: [{ _id: 'Push_enable', value: true }, { _id: 'Push_enable_gateway', value: true }] }
+	@add 'Push_production', true, { type: 'boolean', public: true, enableQuery: [{ _id: 'Push_enable', value: true }, { _id: 'Push_enable_gateway', value: false }] }
+	@add 'Push_test_push', 'push_test', { type: 'action', actionText: 'Send_a_test_push_to_my_user', enableQuery: { _id: 'Push_enable', value: true } }
 
 	@section 'Certificates_and_Keys', ->
 		@add 'Push_apn_passphrase', '', { type: 'string' }
diff --git a/packages/rocketchat-livechat/app/i18n/cs.i18n.json b/packages/rocketchat-livechat/app/i18n/cs.i18n.json
index 9951caca7c162eb1f168e6c545d9cecd4cdcdc83..b602548fbb4deedc3e7d8292391dfc6134bf3bab 100644
--- a/packages/rocketchat-livechat/app/i18n/cs.i18n.json
+++ b/packages/rocketchat-livechat/app/i18n/cs.i18n.json
@@ -33,5 +33,6 @@
   "User_left" : "Uživatel odešel",
   "We_are_offline_Sorry_for_the_inconvenience" : "Jsme offline. Omluváme se za nepříjemnosti.",
   "Yes" : "Ano",
+  "You" : "Vy",
   "You_must_complete_all_fields" : "Je potřeba vyplnit všechna pole"
 }
\ No newline at end of file
diff --git a/packages/rocketchat-livechat/client/collections/livechatOfficeHour.js b/packages/rocketchat-livechat/client/collections/livechatOfficeHour.js
new file mode 100644
index 0000000000000000000000000000000000000000..a741186d23577420a1060471b69531e253bbe80e
--- /dev/null
+++ b/packages/rocketchat-livechat/client/collections/livechatOfficeHour.js
@@ -0,0 +1 @@
+this.LivechatOfficeHour = new Mongo.Collection('rocketchat_livechat_office_hour');
\ No newline at end of file
diff --git a/packages/rocketchat-livechat/client/route.js b/packages/rocketchat-livechat/client/route.js
index 44b379b40fe2f5d1e95d01ce80edbd9fc4c7cf92..1958f4e4d44a706c085412a818623bdccebf9359 100644
--- a/packages/rocketchat-livechat/client/route.js
+++ b/packages/rocketchat-livechat/client/route.js
@@ -75,6 +75,14 @@ AccountBox.addRoute({
 	pageTemplate: 'livechatAppearance'
 }, livechatManagerRoutes);
 
+AccountBox.addRoute({
+	name: 'livechat-officeHours',
+	path: '/officeHours',
+	sideNav: 'livechatFlex',
+	i18nPageTitle: 'Office_Hours',
+	pageTemplate: 'livechatOfficeHours'
+}, livechatManagerRoutes);
+
 AccountBox.addRoute({
 	name: 'livechat-customfields',
 	path: '/customfields',
@@ -113,3 +121,5 @@ AccountBox.addRoute({
 	i18nPageTitle: 'Livechat_Queue',
 	pageTemplate: 'livechatQueue'
 });
+
+
diff --git a/packages/rocketchat-livechat/client/views/app/livechatOfficeHours.html b/packages/rocketchat-livechat/client/views/app/livechatOfficeHours.html
new file mode 100644
index 0000000000000000000000000000000000000000..d6f03caacefcc481d00c27b62164efc53def78bb
--- /dev/null
+++ b/packages/rocketchat-livechat/client/views/app/livechatOfficeHours.html
@@ -0,0 +1,59 @@
+<template name="livechatOfficeHours">
+	<div class="livechat-officeHours-div">
+		<form class="rocket-form" id="officeHoursForm">
+
+			<fieldset>
+				<legend>{{_ "Office_hours_enabled"}}</legend>
+				<input type="radio" class="preview-settings" name="enableOfficeHours" id="enableOfficeHoursTrue" checked="{{enableOfficeHoursTrueChecked}}" value="true">
+				<label for="displayOfflineFormTrue">{{_ "True"}}</label>
+				<input type="radio" class="preview-settings" name="enableOfficeHours" id="enableOfficeHoursFalse" checked="{{enableOfficeHoursFalseChecked}}" value="false">
+				<label for="displayOfflineFormFalse">{{_ "False"}}</label>
+			</fieldset>
+
+			<!-- days open -->
+			<fieldset>
+				<legend>{{_ "Open_days_of_the_week"}}</legend>
+				{{#each day in days}}
+					{{#if open day}}
+						<label class="dayOpenCheck"><input type="checkbox" name={{openName day}} checked>{{name day}}</label>
+					{{else}}
+						<label class="dayOpenCheck"><input type="checkbox" name={{openName day}}>{{name day}}</label>
+					{{/if}}
+				{{/each}}
+			</fieldset>
+
+			<!-- times -->
+			<fieldset>
+				<legend>{{_ "Hours"}}</legend>
+				{{#each day in days}}
+					<div class="input-line">
+						<h1><strong>{{name day}}</strong></h1>
+						<table style="width:100%;">
+							<tr>
+								<td>{{_ "Open"}}:</td>
+								<td>{{_ "Close"}}:</td>
+							</tr>
+							<tr>
+								<td>
+									<div style="margin-right:30px">
+										<input type="time" class="preview-settings" name={{startName day}} id={{startName day}}  value={{start day}} style="width=100px;">
+									</div>
+								</td>
+								<td>
+									<div style="margin-right:30px">
+										<input type="time" class="preview-settings" name={{finishName day}} id={{finishName day}} value={{finish day}} style="width=100px;">
+									</div>
+								</td>
+							</tr>
+						</table>
+					</div>
+				{{/each}}
+			</fieldset>
+
+
+			<div class="submit">
+				<button class="button"><i class="icon-floppy"></i>{{_ "Save"}}</button>
+			</div>
+		</form>
+	</div>
+</template>
\ No newline at end of file
diff --git a/packages/rocketchat-livechat/client/views/app/livechatOfficeHours.js b/packages/rocketchat-livechat/client/views/app/livechatOfficeHours.js
new file mode 100644
index 0000000000000000000000000000000000000000..298e84d4d11e06bc75a0673cd8f7023f4c740ef6
--- /dev/null
+++ b/packages/rocketchat-livechat/client/views/app/livechatOfficeHours.js
@@ -0,0 +1,157 @@
+/* globals LivechatOfficeHour */
+
+Template.livechatOfficeHours.helpers({
+	days() {
+		return LivechatOfficeHour.find();
+	},
+	startName(day) {
+		return day.day + '_start';
+	},
+	finishName(day) {
+		return day.day + '_finish';
+	},
+	openName(day) {
+		return day.day + '_open';
+	},
+	start(day) {
+		return Template.instance().dayVars[day.day].start.get();
+	},
+	finish(day) {
+		return Template.instance().dayVars[day.day].finish.get();
+
+	},
+	name(day) {
+		return day.day;
+	},
+	open(day) {
+		return Template.instance().dayVars[day.day].open.get();
+	},
+	enableOfficeHoursTrueChecked() {
+		if (Template.instance().enableOfficeHours.get()) {
+			return 'checked';
+		}
+	},
+	enableOfficeHoursFalseChecked() {
+		if (!Template.instance().enableOfficeHours.get()) {
+			return 'checked';
+		}
+	}
+});
+
+Template.livechatOfficeHours.events({
+	'change .preview-settings, keydown .preview-settings'(e, instance) {
+		var temp = e.currentTarget.name.split('_');
+
+		var newTime = moment(e.currentTarget.value, 'HH:mm');
+
+		// check if start and stop do not cross
+		if (temp[1] === 'start') {
+			if (newTime.isSameOrBefore(moment(instance.dayVars[temp[0]].finish.get(), 'HH:mm'))) {
+				instance.dayVars[temp[0]].start.set(e.currentTarget.value);
+			} else {
+				e.currentTarget.value = instance.dayVars[temp[0]].start.get();
+			}
+		} else if (temp[1] === 'finish') {
+			if (newTime.isSameOrAfter(moment(instance.dayVars[temp[0]].start.get(), 'HH:mm'))) {
+				instance.dayVars[temp[0]].finish.set(e.currentTarget.value);
+			} else {
+				e.currentTarget.value = instance.dayVars[temp[0]].finish.get();
+			}
+		}
+	},
+	'change .dayOpenCheck input'(e, instance) {
+		var temp = e.currentTarget.name.split('_');
+		instance.dayVars[temp[0]][temp[1]].set(e.target.checked);
+	},
+	'change .preview-settings, keyup .preview-settings'(e, instance) {
+		let value = e.currentTarget.value;
+		if (e.currentTarget.type === 'radio') {
+			value = value === 'true';
+		}
+
+		instance[e.currentTarget.name].set(value);
+	},
+	'submit .rocket-form'(e, instance) {
+		e.preventDefault();
+
+		// convert all times to utc then update them in db
+		for (var d in instance.dayVars) {
+			if (instance.dayVars.hasOwnProperty(d)) {
+				var day = instance.dayVars[d];
+				var start_utc = moment(day.start.get(), 'HH:mm').utc().format('HH:mm');
+				var finish_utc = moment(day.finish.get(), 'HH:mm').utc().format('HH:mm');
+
+				Meteor.call('livechat:saveOfficeHours', d, start_utc, finish_utc, day.open.get(), function(err /*,result*/) {
+					if (err) {
+						return handleError(err);
+					}
+				});
+			}
+		}
+
+		RocketChat.settings.set('Livechat_enable_office_hours', instance.enableOfficeHours.get(), (err/*, success*/) => {
+			if (err) {
+				return handleError(err);
+			}
+			toastr.success(t('Office_Hours_updated'));
+		});
+	}
+});
+
+Template.livechatOfficeHours.onCreated(function() {
+	this.dayVars = {
+		Monday: {
+			start: new ReactiveVar('08:00'),
+			finish: new ReactiveVar('20:00'),
+			open: new ReactiveVar(true)
+		},
+		Tuesday: {
+			start: new ReactiveVar('00:00'),
+			finish: new ReactiveVar('00:00'),
+			open: new ReactiveVar(true)
+		},
+		Wednesday: {
+			start: new ReactiveVar('00:00'),
+			finish: new ReactiveVar('00:00'),
+			open: new ReactiveVar(true)
+		},
+		Thursday: {
+			start: new ReactiveVar('00:00'),
+			finish: new ReactiveVar('00:00'),
+			open: new ReactiveVar(true)
+		},
+		Friday: {
+			start: new ReactiveVar('00:00'),
+			finish: new ReactiveVar('00:00'),
+			open: new ReactiveVar(true)
+		},
+		Saturday: {
+			start: new ReactiveVar('00:00'),
+			finish: new ReactiveVar('00:00'),
+			open: new ReactiveVar(false)
+		},
+		Sunday: {
+			start: new ReactiveVar('00:00'),
+			finish: new ReactiveVar('00:00'),
+			open: new ReactiveVar(false)
+		}
+	};
+
+	this.autorun(() => {
+		this.subscribe('livechat:officeHour');
+
+		if (this.subscriptionsReady()) {
+			LivechatOfficeHour.find().forEach(function(d) {
+				Template.instance().dayVars[d.day].start.set(moment.utc(d.start, 'HH:mm').local().format('HH:mm'));
+				Template.instance().dayVars[d.day].finish.set(moment.utc(d.finish, 'HH:mm').local().format('HH:mm'));
+				Template.instance().dayVars[d.day].open.set(d.open);
+			});
+		}
+	});
+
+	this.enableOfficeHours = new ReactiveVar(null);
+
+	this.autorun(() => {
+		this.enableOfficeHours.set(RocketChat.settings.get('Livechat_enable_office_hours'));
+	});
+});
\ No newline at end of file
diff --git a/packages/rocketchat-livechat/client/views/sideNav/livechatFlex.html b/packages/rocketchat-livechat/client/views/sideNav/livechatFlex.html
index d607c7bc323400eb8f19f53607d247f2696f62b6..064a644e0d1256785749a98192d43c84b1d2f11c 100644
--- a/packages/rocketchat-livechat/client/views/sideNav/livechatFlex.html
+++ b/packages/rocketchat-livechat/client/views/sideNav/livechatFlex.html
@@ -17,6 +17,7 @@
 					<a href="{{pathFor 'livechat-installation'}}" class="{{active 'livechat-installation'}}">{{_ "Installation"}}</a>
 					<a href="{{pathFor 'livechat-appearance'}}" class="{{active 'livechat-appearance'}}">{{_ "Appearance"}}</a>
 					<a href="{{pathFor 'livechat-integrations'}}" class="{{active 'livechat-integrations'}}">{{_ "Integrations"}}</a>
+					<a href="{{pathFor 'livechat-officeHours'}}" class="{{active 'livechat-officeHours'}}">{{_ "Office_Hours"}}</a>
 				</li>
 			</ul>
 		</div>
diff --git a/packages/rocketchat-livechat/config.js b/packages/rocketchat-livechat/config.js
index 01879dd9140626d91bf6226cdbea84c5faa85d1e..22ef78651f3081b7a71ce577f1aa049b5335141a 100644
--- a/packages/rocketchat-livechat/config.js
+++ b/packages/rocketchat-livechat/config.js
@@ -164,4 +164,11 @@ Meteor.startup(function() {
 		public: true,
 		i18nLabel: 'Show_queue_list_to_all_agents'
 	});
+
+	RocketChat.settings.add('Livechat_enable_office_hours', false, {
+		type: 'boolean',
+		group: 'Livechat',
+		public: true,
+		i18nLabel: 'Office_Hours_Enabled'
+	});
 });
diff --git a/packages/rocketchat-livechat/package.js b/packages/rocketchat-livechat/package.js
index bcd13384d4a89ff3ed66867be75a67a6437660c9..04728ca4b3c3edd37f7dc4bbbaf8ab35fb6e873d 100644
--- a/packages/rocketchat-livechat/package.js
+++ b/packages/rocketchat-livechat/package.js
@@ -62,6 +62,7 @@ Package.onUse(function(api) {
 	api.addFiles('client/collections/LivechatQueueUser.js', 'client');
 	api.addFiles('client/collections/LivechatTrigger.js', 'client');
 	api.addFiles('client/collections/LivechatInquiry.js', 'client');
+	api.addFiles('client/collections/livechatOfficeHour.js', 'client');
 
 	api.addFiles('client/methods/changeLivechatStatus.js', 'client');
 
@@ -90,6 +91,8 @@ Package.onUse(function(api) {
 	api.addFiles('client/views/app/livechatTriggers.js', 'client');
 	api.addFiles('client/views/app/livechatUsers.html', 'client');
 	api.addFiles('client/views/app/livechatUsers.js', 'client');
+	api.addFiles('client/views/app/livechatOfficeHours.html', 'client');
+	api.addFiles('client/views/app/livechatOfficeHours.js', 'client');
 
 	api.addFiles('client/views/app/tabbar/externalSearch.html', 'client');
 	api.addFiles('client/views/app/tabbar/externalSearch.js', 'client');
@@ -149,6 +152,7 @@ Package.onUse(function(api) {
 	api.addFiles('server/methods/webhookTest.js', 'server');
 	api.addFiles('server/methods/takeInquiry.js', 'server');
 	api.addFiles('server/methods/returnAsInquiry.js', 'server');
+	api.addFiles('server/methods/saveOfficeHours.js', 'server');
 
 	// models
 	api.addFiles('server/models/Users.js', 'server');
@@ -161,10 +165,12 @@ Package.onUse(function(api) {
 	api.addFiles('server/models/LivechatTrigger.js', 'server');
 	api.addFiles('server/models/indexes.js', 'server');
 	api.addFiles('server/models/LivechatInquiry.js', 'server');
+	api.addFiles('server/models/LivechatOfficeHour.js', 'server');
 
 	// server lib
 	api.addFiles('server/lib/Livechat.js', 'server');
 	api.addFiles('server/lib/QueueMethods.js', 'server');
+	api.addFiles('server/lib/OfficeClock.js', 'server');
 
 	api.addFiles('server/sendMessageBySMS.js', 'server');
 	api.addFiles('server/forwardUnclosedLivechats.js', 'server');
@@ -183,6 +189,7 @@ Package.onUse(function(api) {
 	api.addFiles('server/publications/visitorInfo.js', 'server');
 	api.addFiles('server/publications/visitorPageVisited.js', 'server');
 	api.addFiles('server/publications/livechatInquiries.js', 'server');
+	api.addFiles('server/publications/livechatOfficeHours.js', 'server');
 
 	// api
 	api.addFiles('server/api.js', 'server');
diff --git a/packages/rocketchat-livechat/server/lib/OfficeClock.js b/packages/rocketchat-livechat/server/lib/OfficeClock.js
new file mode 100644
index 0000000000000000000000000000000000000000..24591730e71a659f577ddd2da3eda3faf9506398
--- /dev/null
+++ b/packages/rocketchat-livechat/server/lib/OfficeClock.js
@@ -0,0 +1,10 @@
+// Every minute check if office closed
+Meteor.setInterval(function() {
+	if (RocketChat.settings.get('Livechat_enable_office_hours')) {
+		if (RocketChat.models.LivechatOfficeHour.isOpeningTime()) {
+			RocketChat.models.Users.openOffice();
+		} else if (RocketChat.models.LivechatOfficeHour.isClosingTime()) {
+			RocketChat.models.Users.closeOffice();
+		}
+	}
+}, 60000);
\ No newline at end of file
diff --git a/packages/rocketchat-livechat/server/methods/saveOfficeHours.js b/packages/rocketchat-livechat/server/methods/saveOfficeHours.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc3b57fd16775843032b60e8909f62b23684234c
--- /dev/null
+++ b/packages/rocketchat-livechat/server/methods/saveOfficeHours.js
@@ -0,0 +1,5 @@
+Meteor.methods({
+	'livechat:saveOfficeHours'(day, start, finish, open) {
+		RocketChat.models.LivechatOfficeHour.updateHours(day, start, finish, open);
+	}
+});
\ No newline at end of file
diff --git a/packages/rocketchat-livechat/server/models/LivechatOfficeHour.js b/packages/rocketchat-livechat/server/models/LivechatOfficeHour.js
new file mode 100644
index 0000000000000000000000000000000000000000..ef77da9a560f1c946994f9921d64d3c6be4927eb
--- /dev/null
+++ b/packages/rocketchat-livechat/server/models/LivechatOfficeHour.js
@@ -0,0 +1,110 @@
+class LivechatOfficeHour extends RocketChat.models._Base {
+	constructor() {
+		super();
+		this._initModel('livechat_office_hour');
+
+		this.tryEnsureIndex({ 'day': 1 }); // the day of the week monday - sunday
+		this.tryEnsureIndex({ 'start': 1 }); // the opening hours of the office
+		this.tryEnsureIndex({ 'finish': 1 }); // the closing hours of the office
+		this.tryEnsureIndex({ 'open': 1 }); // whether or not the offices are open on this day
+
+		// if there is nothing in the collection, add defaults
+		if (this.find().count() === 0) {
+			this.insert({'day' : 'Monday', 'start' : '08:00', 'finish' : '20:00', 'code' : 1, 'open' : true });
+			this.insert({'day' : 'Tuesday', 'start' : '08:00', 'finish' : '20:00', 'code' : 2, 'open' : true });
+			this.insert({'day' : 'Wednesday', 'start' : '08:00', 'finish' : '20:00', 'code' : 3, 'open' : true });
+			this.insert({'day' : 'Thursday', 'start' : '08:00', 'finish' : '20:00', 'code' : 4, 'open' : true });
+			this.insert({'day' : 'Friday', 'start' : '08:00', 'finish' : '20:00', 'code' : 5, 'open' : true });
+			this.insert({'day' : 'Saturday', 'start' : '08:00', 'finish' : '20:00', 'code' : 6, 'open' : false });
+			this.insert({'day' : 'Sunday', 'start' : '08:00', 'finish' : '20:00', 'code' : 0, 'open' : false });
+
+		}
+	}
+
+	/*
+	 * update the given days start and finish times and whether the office is open on that day
+	 */
+	updateHours(day, newStart, newFinish, newOpen) {
+		this.update({
+			'day': day
+		}, {
+			$set: {
+				start: newStart,
+				finish: newFinish,
+				open: newOpen
+			}
+		});
+	}
+
+	/*
+	 * Check if the current server time (utc) is within the office hours of that day
+	 * returns true or false
+	 */
+	isNowWithinHours() {
+		// get current time on server in utc
+		// var ct = moment().utc();
+		var currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm');
+
+		// get todays office hours from db
+		var todaysOfficeHours = this.findOne({day: currentTime.format('dddd')});
+		if (!todaysOfficeHours) {
+			return false;
+		}
+
+		// check if offices are open today
+		if (todaysOfficeHours.open === false) {
+			return false;
+		}
+
+		var start = moment.utc(todaysOfficeHours.day + ':' + todaysOfficeHours.start, 'dddd:HH:mm');
+		var finish = moment.utc(todaysOfficeHours.day + ':' + todaysOfficeHours.finish, 'dddd:HH:mm');
+
+		// console.log(finish.isBefore(start));
+		if (finish.isBefore(start)) {
+			// finish.day(finish.day()+1);
+			finish.add(1, 'days');
+		}
+
+		var result = currentTime.isBetween(start, finish);
+
+		// inBetween  check
+		return result;
+	}
+
+	isOpeningTime() {
+		// get current time on server in utc
+		var currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm');
+
+		// get todays office hours from db
+		var todaysOfficeHours = this.findOne({day: currentTime.format('dddd')});
+		if (!todaysOfficeHours) {
+			return false;
+		}
+
+		// check if offices are open today
+		if (todaysOfficeHours.open === false) {
+			return false;
+		}
+
+		var start = moment.utc(todaysOfficeHours.day + ':' + todaysOfficeHours.start, 'dddd:HH:mm');
+
+		return start.isSame(currentTime, 'minute');
+	}
+
+	isClosingTime() {
+		// get current time on server in utc
+		var currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm');
+
+		// get todays office hours from db
+		var todaysOfficeHours = this.findOne({day: currentTime.format('dddd')});
+		if (!todaysOfficeHours) {
+			return false;
+		}
+
+		var finish = moment.utc(todaysOfficeHours.day + ':' + todaysOfficeHours.finish, 'dddd:HH:mm');
+
+		return finish.isSame(currentTime, 'minute');
+	}
+}
+
+RocketChat.models.LivechatOfficeHour = new LivechatOfficeHour();
diff --git a/packages/rocketchat-livechat/server/models/Rooms.js b/packages/rocketchat-livechat/server/models/Rooms.js
index c507791ec91b93b533b16c1486d1cce51bf9f3ec..5e585f412b030210191c47ae5e56cea1000b1ead 100644
--- a/packages/rocketchat-livechat/server/models/Rooms.js
+++ b/packages/rocketchat-livechat/server/models/Rooms.js
@@ -18,7 +18,8 @@ RocketChat.models.Rooms.updateSurveyFeedbackById = function(_id, surveyFeedback)
 
 RocketChat.models.Rooms.updateLivechatDataByToken = function(token, key, value) {
 	const query = {
-		'v.token': token
+		'v.token': token,
+		open: true
 	};
 
 	const update = {
diff --git a/packages/rocketchat-livechat/server/models/Users.js b/packages/rocketchat-livechat/server/models/Users.js
index f9cfbc3594a866e346ffcfdb4a4f602293f1c65e..a57f738121a485bd20cc481cb36a10ec6f4ddaed 100644
--- a/packages/rocketchat-livechat/server/models/Users.js
+++ b/packages/rocketchat-livechat/server/models/Users.js
@@ -137,6 +137,26 @@ RocketChat.models.Users.setLivechatStatus = function(userId, status) {
 	return this.update(query, update);
 };
 
+/**
+ * change all livechat agents livechat status to "not-available"
+ */
+RocketChat.models.Users.closeOffice = function() {
+	self = this;
+	self.findAgents().forEach(function(agent) {
+		self.setLivechatStatus(agent._id, 'not-available');
+	});
+};
+
+/**
+ * change all livechat agents livechat status to "available"
+ */
+RocketChat.models.Users.openOffice = function() {
+	self = this;
+	self.findAgents().forEach(function(agent) {
+		self.setLivechatStatus(agent._id, 'available');
+	});
+};
+
 RocketChat.models.Users.updateLivechatDataByToken = function(token, key, value) {
 	const query = {
 		'profile.token': token
diff --git a/packages/rocketchat-livechat/server/publications/livechatOfficeHours.js b/packages/rocketchat-livechat/server/publications/livechatOfficeHours.js
new file mode 100644
index 0000000000000000000000000000000000000000..b81ff8a53b0475fdfb9b4f79ae2eb6e8380c7593
--- /dev/null
+++ b/packages/rocketchat-livechat/server/publications/livechatOfficeHours.js
@@ -0,0 +1,7 @@
+Meteor.publish('livechat:officeHour', function() {
+	if (!RocketChat.authz.hasPermission(this.userId, 'view-l-room')) {
+		return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:agents' }));
+	}
+
+	return RocketChat.models.LivechatOfficeHour.find();
+});
\ No newline at end of file
diff --git a/packages/rocketchat-oembed/client/oembedSandstormGrain.html b/packages/rocketchat-oembed/client/oembedSandstormGrain.html
index be0533600ec5a5889f92b78bac1a92ddb77a38be..97157a6b0b7c82fb45341fc28f8fd21b2de5f8d5 100644
--- a/packages/rocketchat-oembed/client/oembedSandstormGrain.html
+++ b/packages/rocketchat-oembed/client/oembedSandstormGrain.html
@@ -1,12 +1,11 @@
 <template name="oembedSandstormGrain">
 	<blockquote class="sandstorm-grain">
 		<label>
-			<h3>{{grainTitle}}</h3>
-			<img src="{{appIconUrl}}" />
 			<button onclick="sandstormOembed(event)" data-token="{{token}}"
 							data-descriptor="{{descriptor}}">
-				Click to open the grain
+				{{grainTitle}}
 			</button>
+			<img src="{{appIconUrl}}" />
 		</label>
 	</blockquote>
 </template>
diff --git a/packages/rocketchat-slackbridge/slackbridge.js b/packages/rocketchat-slackbridge/slackbridge.js
index d093c317238b4f3827f9af3816212ff1c40afa6b..5285493ee35f22bfde9ab7ee51add77525266774 100644
--- a/packages/rocketchat-slackbridge/slackbridge.js
+++ b/packages/rocketchat-slackbridge/slackbridge.js
@@ -87,10 +87,12 @@ class SlackBridge {
 	}
 
 	findChannel(channelId) {
+		logger.class.debug('Searching for Rocket.Chat channel', channelId);
 		return RocketChat.models.Rooms.findOneByImportId(channelId);
 	}
 
 	addChannel(channelId, hasRetried = false) {
+		logger.class.debug('Adding channel from Slack', channelId);
 		let data = null;
 		let isGroup = false;
 		if (channelId.charAt(0) === 'C') {
@@ -102,10 +104,9 @@ class SlackBridge {
 		if (data && data.data && data.data.ok === true) {
 			let channelData = isGroup ? data.data.group : data.data.channel;
 			let existingRoom = RocketChat.models.Rooms.findOneByName(channelData.name);
+
+			// If the room exists, make sure we have its id in importIds
 			if (existingRoom || channelData.is_general) {
-				if (channelData.is_general && channelData.name !== (existingRoom && existingRoom.name)) {
-					Meteor.call('saveRoomSettings', 'GENERAL', 'roomName', channelData.name);
-				}
 				channelData.rocketId = channelData.is_general ? 'GENERAL' : existingRoom._id;
 				RocketChat.models.Rooms.update({ _id: channelData.rocketId }, { $addToSet: { importIds: channelData.id } });
 			} else {
@@ -113,32 +114,28 @@ class SlackBridge {
 				for (let member of channelData.members) {
 					if (member !== channelData.creator) {
 						let user = this.findUser(member) || this.addUser(member);
-						if (user) {
+						if (user && user.username) {
 							users.push(user.username);
 						}
 					}
 				}
 				let creator = channelData.creator ? this.findUser(channelData.creator) || this.addUser(channelData.creator) : null;
 				if (!creator) {
-					logger.events.error('Could not fetch room creator information', channelData.creator);
+					logger.class.error('Could not fetch room creator information', channelData.creator);
 					return;
 				}
 
 				try {
-					Meteor.runAsUser(creator._id, () => {
-						if (isGroup) {
-							let channel = Meteor.call('createPrivateGroup', channelData.name, users);
-							channelData.rocketId = channel._id;
-						} else {
-							let channel = Meteor.call('createChannel', channelData.name, users);
-							channelData.rocketId = channel._id;
-						}
-					});
+					let channel = RocketChat.createRoom(isGroup ? 'p' : 'c', channelData.name, creator.username, users);
+					channelData.rocketId = channel.rid;
 				} catch (e) {
 					if (!hasRetried) {
+						logger.class.debug('Error adding channel from Slack. Will retry in 1s.', e.message);
 						// If first time trying to create channel fails, could be because of multiple messages received at the same time. Try again once after 1s.
 						Meteor._sleepForMs(1000);
 						return this.findChannel(channelId) || this.addChannel(channelId, true);
+					} else {
+						console.log(e.message);
 					}
 				}
 
@@ -164,6 +161,7 @@ class SlackBridge {
 	}
 
 	findUser(userId) {
+		logger.class.debug('Searching for Rocket.Chat user', userId);
 		let user = RocketChat.models.Users.findOneByImportId(userId);
 		if (user && !this.userTags[userId]) {
 			this.userTags[userId] = { slack: `<@${userId}>`, rocket: `@${user.username}` };
@@ -172,6 +170,7 @@ class SlackBridge {
 	}
 
 	addUser(userId) {
+		logger.class.debug('Adding user from Slack', userId);
 		let data = HTTP.get('https://slack.com/api/users.info', { params: { token: this.apiToken, user: userId } });
 		if (data && data.data && data.data.ok === true && data.data.user && data.data.user.profile && data.data.user.profile.email) {
 			let userData = data.data.user;
@@ -181,37 +180,47 @@ class SlackBridge {
 				userData.name = existingUser.username;
 			} else {
 				userData.rocketId = Accounts.createUser({ email: userData.profile.email, password: Date.now() + userData.name + userData.profile.email.toUpperCase() });
-				Meteor.runAsUser(userData.rocketId, () => {
-					Meteor.call('setUsername', userData.name);
-					Meteor.call('joinDefaultChannels', true);
-					let url = null;
-					if (userData.profile.image_original) {
-						url = userData.profile.image_original;
-					} else if (userData.profile.image_512) {
-						url = userData.profile.image_512;
-					}
-					Meteor.call('setAvatarFromService', url, null, 'url');
-					// Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600
-					if (userData.tz_offset) {
-						Meteor.call('updateUserUtcOffset', userData.tz_offset / 3600);
-					}
-					if (userData.profile.real_name) {
-						RocketChat.models.Users.setName(userData.rocketId, userData.profile.real_name);
-					}
-				});
-				// Deleted users are 'inactive' users in Rocket.Chat
+				let userUpdate = {
+					username: userData.name,
+					utcOffset: userData.tz_offset / 3600, // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600,
+					roles: [ 'user' ]
+				};
+
+				if (userData.profile.real_name) {
+					userUpdate['name'] = userData.profile.real_name;
+				}
+
 				if (userData.deleted) {
-					RocketChat.models.Users.setUserActive(userData.rocketId, false);
-					RocketChat.models.Users.unsetLoginTokens(userData.rocketId);
+					userUpdate['active'] = false;
+					userUpdate['services.resume.loginTokens'] = [];
 				}
+
+				RocketChat.models.Users.update({ _id: userData.rocketId }, { $set: userUpdate });
+
+				let user = RocketChat.models.Users.findOneById(userData.rocketId);
+
+				let url = null;
+				if (userData.profile.image_original) {
+					url = userData.profile.image_original;
+				} else if (userData.profile.image_512) {
+					url = userData.profile.image_512;
+				}
+				try {
+					RocketChat.setUserAvatar(user, url, null, 'url');
+				} catch (error) {
+					logger.class.debug('Error setting user avatar', error.message);
+				}
+				RocketChat.addUserToDefaultChannels(user, true);
 			}
+
 			RocketChat.models.Users.update({ _id: userData.rocketId }, { $addToSet: { importIds: userData.id } });
 			if (!this.userTags[userId]) {
 				this.userTags[userId] = { slack: `<@${userId}>`, rocket: `@${userData.name}` };
 			}
+			logger.class.debug('User: ', userData.rocketId);
 			return RocketChat.models.Users.findOneById(userData.rocketId);
 		}
-
+		logger.class.debug('User not added');
 		return;
 	}
 
@@ -227,11 +236,11 @@ class SlackBridge {
 		return msgObj;
 	}
 
-	sendMessage(room, user, message, msgDataDefaults) {
+	sendMessage(room, user, message, msgDataDefaults, importing) {
 		if (message.type === 'message') {
 			let msgObj = {};
 			if (!_.isEmpty(message.subtype)) {
-				msgObj = this.processSubtypedMessage(room, user, message, msgDataDefaults);
+				msgObj = this.processSubtypedMessage(room, user, message, importing);
 				if (!msgObj) {
 					return;
 				}
@@ -264,7 +273,7 @@ class SlackBridge {
 		}
 	}
 
-	saveMessage(message) {
+	saveMessage(message, importing) {
 		let channel = message.channel ? this.findChannel(message.channel) || this.addChannel(message.channel) : null;
 		let user = null;
 		if (message.subtype === 'message_deleted' || message.subtype === 'message_changed') {
@@ -277,11 +286,24 @@ class SlackBridge {
 				_id: `slack-${message.channel}-${message.ts.replace(/\./g, '-')}`,
 				ts: new Date(parseInt(message.ts.split('.')[0]) * 1000)
 			};
-			this.sendMessage(channel, user, message, msgDataDefaults);
+			if (importing) {
+				msgDataDefaults['imported'] = 'slackbridge';
+			}
+			try {
+				this.sendMessage(channel, user, message, msgDataDefaults, importing);
+			} catch (e) {
+				// http://www.mongodb.org/about/contributors/error-codes/
+				// 11000 == duplicate key error
+				if (e.name === 'MongoError' && e.code === 11000) {
+					return;
+				}
+
+				throw e;
+			}
 		}
 	}
 
-	processSubtypedMessage(room, user, message) {
+	processSubtypedMessage(room, user, message, importing) {
 		let msgObj = null;
 		switch (message.subtype) {
 			case 'bot_message':
@@ -309,45 +331,84 @@ class SlackBridge {
 				this.editMessage(room, user, message);
 				return;
 			case 'message_deleted':
-				msgObj = RocketChat.models.Messages.findOneById(`${message.channel}S${message.deleted_ts}`);
-				if (msgObj) {
-					Meteor.runAsUser(user._id, () => {
-						Meteor.call('deleteMessage', msgObj);
-					});
+				if (message.previous_message) {
+					let _id = `slack-${message.channel}-${message.previous_message.ts.replace(/\./g, '-')}`;
+					msgObj = RocketChat.models.Messages.findOneById(_id);
+					if (msgObj) {
+						RocketChat.deleteMessage(msgObj, user);
+					}
 				}
 				return;
 			case 'channel_join':
-				return this.joinRoom(room, user);
+				if (importing) {
+					RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(room._id, user, { ts: new Date(parseInt(message.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
+				} else {
+					RocketChat.addUserToRoom(room._id, user);
+				}
+				return;
 			case 'group_join':
 				if (message.inviter) {
 					let inviter = message.inviter ? this.findUser(message.inviter) || this.addUser(message.inviter) : null;
-					if (inviter) {
-						return this.joinPrivateGroup(inviter, room, user);
+					if (importing) {
+						RocketChat.models.Messages.createUserAddedWithRoomIdAndUser(room._id, user, {
+							ts: new Date(parseInt(message.ts.split('.')[0]) * 1000),
+							u: {
+								_id: inviter._id,
+								username: inviter.username
+							},
+							imported: 'slackbridge'
+						});
+					} else {
+						RocketChat.addUserToRoom(room._id, user, inviter);
 					}
 				}
-				break;
+				return;
 			case 'channel_leave':
 			case 'group_leave':
-				return this.leaveRoom(room, user);
+				if (importing) {
+					RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(room._id, user, {
+						ts: new Date(parseInt(message.ts.split('.')[0]) * 1000),
+						imported: 'slackbridge'
+					});
+				} else {
+					RocketChat.removeUserFromRoom(room._id, user);
+				}
+				return;
 			case 'channel_topic':
 			case 'group_topic':
-				this.setRoomTopic(room, user, message.topic);
+				if (importing) {
+					RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', room._id, message.topic, user, { ts: new Date(parseInt(message.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
+				} else {
+					RocketChat.saveRoomTopic(room._id, message.topic, user);
+				}
 				return;
 			case 'channel_purpose':
 			case 'group_purpose':
-				this.setRoomTopic(room, user, message.purpose);
+				if (importing) {
+					RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', room._id, message.purpose, user, { ts: new Date(parseInt(message.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
+				} else {
+					RocketChat.saveRoomTopic(room._id, message.purpose, user);
+				}
 				return;
 			case 'channel_name':
 			case 'group_name':
-				this.setRoomName(room, user, message.name);
+				if (importing) {
+					RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(room._id, message.name, user, { ts: new Date(parseInt(message.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
+				} else {
+					RocketChat.saveRoomName(room._id, message.name, user);
+				}
 				return;
 			case 'channel_archive':
 			case 'group_archive':
-				this.archiveRoom(room, user);
+				if (!importing) {
+					RocketChat.archiveRoom(room);
+				}
 				return;
 			case 'channel_unarchive':
 			case 'group_unarchive':
-				this.unarchiveRoom(room, user);
+				if (!importing) {
+					RocketChat.unarchiveRoom(room);
+				}
 				return;
 			case 'file_share':
 				if (message.file && message.file.url_private_download !== undefined) {
@@ -358,14 +419,14 @@ class SlackBridge {
 						type: message.file.mimetype,
 						rid: room._id
 					};
-					return this.uploadFile(details, message.file.url_private_download, user, room, new Date(parseInt(message.ts.split('.')[0]) * 1000));
+					return this.uploadFile(details, message.file.url_private_download, user, room, new Date(parseInt(message.ts.split('.')[0]) * 1000), importing);
 				}
 				break;
 			case 'file_comment':
-				logger.events.error('File comment not implemented');
+				logger.class.error('File comment not implemented');
 				return;
 			case 'file_mention':
-				logger.events.error('File mentioned not implemented');
+				logger.class.error('File mentioned not implemented');
 				return;
 			case 'pinned_item':
 				if (message.attachments && message.attachments[0] && message.attachments[0].text) {
@@ -385,94 +446,33 @@ class SlackBridge {
 						}]
 					};
 
-					RocketChat.models.Messages.setPinnedByIdAndUserId(`slack-${message.attachments[0].channel_id}-${message.attachments[0].ts.replace(/\./g, '-')}`, msgObj.u, true, new Date(parseInt(message.ts.split('.')[0]) * 1000));
+					if (!importing) {
+						RocketChat.models.Messages.setPinnedByIdAndUserId(`slack-${message.attachments[0].channel_id}-${message.attachments[0].ts.replace(/\./g, '-')}`, msgObj.u, true, new Date(parseInt(message.ts.split('.')[0]) * 1000));
+					}
 
 					return msgObj;
 				} else {
-					logger.events.error('Pinned item with no attachment');
+					logger.class.error('Pinned item with no attachment');
 				}
 				return;
 			case 'unpinned_item':
-				logger.events.error('Unpinned item not implemented');
+				logger.class.error('Unpinned item not implemented');
 				return;
 		}
 	}
 
-	/**
-	* Archives a room
-	**/
-	archiveRoom(room, user) {
-		Meteor.runAsUser(user._id, () => {
-			return Meteor.call('archiveRoom', room._id);
-		});
-	}
-
-	/**
-	* Unarchives a room
-	**/
-	unarchiveRoom(room, user) {
-		Meteor.runAsUser(user._id, () => {
-			return Meteor.call('unarchiveRoom', room._id);
-		});
-	}
-
-	/**
-	* Adds user to room and sends a message
-	**/
-	joinRoom(room, user) {
-		Meteor.runAsUser(user._id, () => {
-			return Meteor.call('joinRoom', room._id);
-		});
-	}
-
-	/**
-	* Adds user to room and sends a message
-	**/
-	joinPrivateGroup(inviter, room, user) {
-		Meteor.runAsUser(inviter._id, () => {
-			return Meteor.call('addUserToRoom', { rid: room._id, username: user.username });
-		});
-	}
-
-	/**
-	* Removes user from room and sends a message
-	**/
-	leaveRoom(room, user) {
-		Meteor.runAsUser(user._id, () => {
-			return Meteor.call('leaveRoom', room._id);
-		});
-	}
-
-	/**
-	* Sets room topic
-	**/
-	setRoomTopic(room, user, topic) {
-		Meteor.runAsUser(user._id, () => {
-			return Meteor.call('saveRoomSettings', room._id, 'roomTopic', topic);
-		});
-	}
-
-	/**
-	* Sets room name
-	**/
-	setRoomName(room, user, name) {
-		Meteor.runAsUser(user._id, () => {
-			return Meteor.call('saveRoomSettings', room._id, 'roomName', name);
-		});
-	}
-
 	/**
 	* Edits a message
 	**/
 	editMessage(room, user, message) {
 		let msgObj = {
-			_id: `${message.channel}S${message.message.ts}`,
+			//@TODO _id
+			_id: `slack-${message.channel}-${message.message.ts.replace(/\./g, '-')}`,
 			rid: room._id,
 			msg: this.convertSlackMessageToRocketChat(message.message.text)
 		};
-		Meteor.runAsUser(user._id, () => {
-			return Meteor.call('updateMessage', msgObj);
-		});
+
+		RocketChat.updateMessage(msgObj, user);
 	}
 
 	/**
@@ -483,7 +483,7 @@ class SlackBridge {
 	@param [Object] room the Rocket.Chat room
 	@param [Date] timeStamp the timestamp the file was uploaded
 	**/
-	uploadFile(details, fileUrl, user, room, timeStamp) {
+	uploadFile(details, fileUrl, user, room, timeStamp, importing) {
 		let url = Npm.require('url');
 		let requestModule = /https/i.test(fileUrl) ? Npm.require('https') : Npm.require('http');
 		var parsedUrl = url.parse(fileUrl, true);
@@ -530,6 +530,10 @@ class SlackBridge {
 							attachments: [attachment]
 						};
 
+						if (importing) {
+							msg.imported = 'slackbridge';
+						}
+
 						if (details.message_id && (typeof details.message_id === 'string')) {
 							msg['_id'] = details.message_id;
 						}
@@ -784,6 +788,7 @@ class SlackBridge {
 	}
 
 	importFromHistory(family, options) {
+		logger.class.debug('Importing messages history');
 		let response = HTTP.get('https://slack.com/api/' + family + '.history', { params: _.extend({ token: this.apiToken }, options) });
 		if (response && response.data && _.isArray(response.data.messages) && response.data.messages.length > 0) {
 			let latest = 0;
@@ -793,28 +798,106 @@ class SlackBridge {
 					latest = message.ts;
 				}
 				message.channel = options.channel;
-				this.saveMessage(message);
+				this.saveMessage(message, true);
 			}
 			return { has_more: response.data.has_more, ts: latest };
 		}
 	}
 
+	copyChannelInfo(rid, channelMap) {
+		logger.class.debug('Copying users from Slack channel to Rocket.Chat', channelMap.id, rid);
+		let response = HTTP.get('https://slack.com/api/' + channelMap.family + '.info', { params: { token: this.apiToken, channel: channelMap.id } });
+		if (response && response.data) {
+			let data = channelMap.family === 'channels' ? response.data.channel : response.data.group;
+			if (data && _.isArray(data.members) && data.members.length > 0) {
+				for (let member of data.members) {
+					let user = this.findUser(member) || this.addUser(member);
+					if (user) {
+						logger.class.debug('Adding user to room', user.username, rid);
+						RocketChat.addUserToRoom(rid, user, null, true);
+					}
+				}
+			}
+
+			let topic = '';
+			let topic_last_set = 0;
+			let topic_creator = null;
+			if (data && data.topic && data.topic.value) {
+				topic = data.topic.value;
+				topic_last_set = data.topic.last_set;
+				topic_creator = data.topic.creator;
+			}
+
+			if (data && data.purpose && data.purpose.value) {
+				if (topic_last_set) {
+					if (topic_last_set < data.purpose.last_set) {
+						topic = data.purpose.topic;
+						topic_creator = data.purpose.creator;
+					}
+				} else {
+					topic = data.purpose.topic;
+					topic_creator = data.purpose.creator;
+				}
+			}
+
+			if (topic) {
+				let creator = this.findUser(topic_creator) || this.addUser(topic_creator);
+				logger.class.debug('Setting room topic', rid, topic, creator.username);
+				RocketChat.saveRoomTopic(rid, topic, creator);
+			}
+		}
+	}
+
+	copyPins(rid, channelMap) {
+		let response = HTTP.get('https://slack.com/api/pins.list', { params: { token: this.apiToken, channel: channelMap.id } });
+		if (response && response.data && _.isArray(response.data.items) && response.data.items.length > 0) {
+			for (let pin of response.data.items) {
+				if (pin.message) {
+					let user = this.findUser(pin.message.user);
+					let msgObj = {
+						rid: rid,
+						t: 'message_pinned',
+						msg: '',
+						u: {
+							_id: user._id,
+							username: user.username
+						},
+						attachments: [{
+							'text' : this.convertSlackMessageToRocketChat(pin.message.text),
+							'author_name' : user.username,
+							'author_icon' : getAvatarUrlFromUsername(user.username),
+							'ts' : new Date(parseInt(pin.message.ts.split('.')[0]) * 1000)
+						}]
+					};
+
+					RocketChat.models.Messages.setPinnedByIdAndUserId(`slack-${pin.channel}-${pin.message.ts.replace(/\./g, '-')}`, msgObj.u, true, new Date(parseInt(pin.message.ts.split('.')[0]) * 1000));
+				}
+			}
+		}
+	}
+
 	importMessages(rid, callback) {
 		logger.class.info('importMessages: ', rid);
 		let rocketchat_room = RocketChat.models.Rooms.findOneById(rid);
 		if (rocketchat_room) {
 			if (this.channelMap[rid]) {
+				this.copyChannelInfo(rid, this.channelMap[rid]);
+
 				logger.class.debug('Importing messages from Slack to Rocket.Chat', this.channelMap[rid], rid);
 				let results = this.importFromHistory(this.channelMap[rid].family, { channel: this.channelMap[rid].id, oldest: 1 });
 				while (results && results.has_more) {
 					results = this.importFromHistory(this.channelMap[rid].family, { channel: this.channelMap[rid].id, oldest: results.ts });
 				}
+
+				logger.class.debug('Pinning Slack channel messages to Rocket.Chat', this.channelMap[rid], rid);
+				this.copyPins(rid, this.channelMap[rid]);
+
 				return callback();
 			} else {
 				let slack_room = this.findSlackChannel(rocketchat_room.name);
 				if (slack_room) {
 					this.channelMap[rid] = { id: slack_room.id, family: slack_room.id.charAt(0) === 'C' ? 'channels' : 'groups' };
-					this.importMessages(rid, callback);
+					return this.importMessages(rid, callback);
 				} else {
 					logger.class.error('Could not find Slack room with specified name', rocketchat_room.name);
 					return callback(new Meteor.Error('error-slack-room-not-found', 'Could not find Slack room with specified name'));
diff --git a/packages/rocketchat-slackbridge/slashcommand/slackbridge_import.server.js b/packages/rocketchat-slackbridge/slashcommand/slackbridge_import.server.js
index ddc8daa0fbaccd28d4945ec2ec9a27edf90b9899..aa50f7051d17d88153b08cea76cb2540ef76004f 100644
--- a/packages/rocketchat-slackbridge/slashcommand/slackbridge_import.server.js
+++ b/packages/rocketchat-slackbridge/slashcommand/slackbridge_import.server.js
@@ -1,3 +1,4 @@
+/* globals msgStream */
 function SlackBridgeImport(command, params, item) {
 	var channel, room, user;
 	if (command !== 'slackbridge-import' || !Match.test(params, String)) {
@@ -6,40 +7,57 @@ function SlackBridgeImport(command, params, item) {
 	room = RocketChat.models.Rooms.findOneById(item.rid);
 	channel = room.name;
 	user = Meteor.users.findOne(Meteor.userId());
-	RocketChat.Notifications.notifyUser(Meteor.userId(), 'message', {
+
+	msgStream.emit(item.rid, {
 		_id: Random.id(),
 		rid: item.rid,
+		u: { username: 'rocket.cat' },
 		ts: new Date(),
-		msg: TAPi18n.__('SlackBridge_is_importing_your_messages_at_s_', {
+		msg: TAPi18n.__('SlackBridge_start', {
 			postProcess: 'sprintf',
-			sprintf: [channel]
+			sprintf: [user.username, channel]
 		}, user.language)
 	});
 
-	RocketChat.SlackBridge.importMessages(item.rid, err => {
-		if (err) {
-			RocketChat.Notifications.notifyUser(user._id, 'message', {
-				_id: Random.id(),
-				rid: item.rid,
-				ts: new Date(),
-				msg: TAPi18n.__('SlackBridge_got_an_error_while_importing_your_messages_at_s__s_', {
-					postProcess: 'sprintf',
-					sprintf: [channel, err.message]
-				}, user.language)
-			});
-		} else {
-			RocketChat.Notifications.notifyUser(user._id, 'message', {
-				_id: Random.id(),
-				rid: item.rid,
-				ts: new Date(),
-				msg: TAPi18n.__('SlackBridge_has_finished_importing_your_messages_at_s_', {
-					postProcess: 'sprintf',
-					sprintf: [channel]
-				}, user.language)
-			});
-		}
-	});
-
+	try {
+		RocketChat.SlackBridge.importMessages(item.rid, error => {
+			if (error) {
+				msgStream.emit(item.rid, {
+					_id: Random.id(),
+					rid: item.rid,
+					u: { username: 'rocket.cat' },
+					ts: new Date(),
+					msg: TAPi18n.__('SlackBridge_error', {
+						postProcess: 'sprintf',
+						sprintf: [channel, error.message]
+					}, user.language)
+				});
+			} else {
+				msgStream.emit(item.rid, {
+					_id: Random.id(),
+					rid: item.rid,
+					u: { username: 'rocket.cat' },
+					ts: new Date(),
+					msg: TAPi18n.__('SlackBridge_finish', {
+						postProcess: 'sprintf',
+						sprintf: [channel]
+					}, user.language)
+				});
+			}
+		});
+	} catch (error) {
+		msgStream.emit(item.rid, {
+			_id: Random.id(),
+			rid: item.rid,
+			u: { username: 'rocket.cat' },
+			ts: new Date(),
+			msg: TAPi18n.__('SlackBridge_error', {
+				postProcess: 'sprintf',
+				sprintf: [channel, error.message]
+			}, user.language)
+		});
+		throw error;
+	}
 	return SlackBridgeImport;
 }
 
diff --git a/packages/rocketchat-smarsh-connector/lib/rocketchat.js b/packages/rocketchat-smarsh-connector/lib/rocketchat.js
new file mode 100644
index 0000000000000000000000000000000000000000..c609f1f3cac59b7b8b843386eed441d62ec70854
--- /dev/null
+++ b/packages/rocketchat-smarsh-connector/lib/rocketchat.js
@@ -0,0 +1 @@
+RocketChat.smarsh = {};
diff --git a/packages/rocketchat-smarsh-connector/package.js b/packages/rocketchat-smarsh-connector/package.js
new file mode 100644
index 0000000000000000000000000000000000000000..2d2ff37b68c82e2c9b9a3813c74083914eda666f
--- /dev/null
+++ b/packages/rocketchat-smarsh-connector/package.js
@@ -0,0 +1,27 @@
+Package.describe({
+	name: 'rocketchat:smarsh-connector',
+	version: '0.0.1',
+	summary: 'Smarsh Connector',
+	git: ''
+});
+
+Package.onUse(function(api) {
+	api.versionsFrom('1.0');
+
+	api.use([
+		'ecmascript',
+		'rocketchat:lib',
+		'underscore',
+		'mrt:moment',
+		'mrt:moment-timezone'
+	]);
+
+	api.addFiles('lib/rocketchat.js', [ 'client', 'server' ]);
+	api.addFiles([
+		'server/settings.js',
+		'server/models/SmarshHistory.js',
+		'server/functions/sendEmail.js',
+		'server/functions/generateEml.js',
+		'server/startup.js'
+	], 'server');
+});
diff --git a/packages/rocketchat-smarsh-connector/server/functions/generateEml.js b/packages/rocketchat-smarsh-connector/server/functions/generateEml.js
new file mode 100644
index 0000000000000000000000000000000000000000..567fa735f4ad2b45c5022ae0a99b2196899632ce
--- /dev/null
+++ b/packages/rocketchat-smarsh-connector/server/functions/generateEml.js
@@ -0,0 +1,113 @@
+const start = '<table style="width: 100%; border: 1px solid; border-collapse: collapse; table-layout: fixed; margin-top: 10px; font-size: 12px; word-break: break-word;"><tbody>';
+const end = '</tbody></table>';
+const opentr = '<tr style="border: 1px solid;">';
+const closetr = '</tr>';
+const open20td = '<td style="border: 1px solid; text-align: center; width: 20%;">';
+const open60td = '<td style="border: 1px solid; text-align: left; width: 60%; padding: 0 5px;">';
+const closetd = '</td>';
+
+function _getLink(attachment) {
+	const url = attachment.title_link.replace(/ /g, '%20');
+
+	if (Meteor.settings.public.sandstorm || url.match(/^(https?:)?\/\//i)) {
+		return url;
+	} else {
+		return Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX + url;
+	}
+}
+
+RocketChat.smarsh.generateEml = () => {
+	Meteor.defer(() => {
+		RocketChat.models.Rooms.find().forEach((room) => {
+			const smarshHistory = RocketChat.smarsh.History.findOne({ _id: room._id });
+			const query = { rid: room._id };
+
+			if (smarshHistory) {
+				query.ts = { $gt: smarshHistory.lastRan };
+			}
+
+			const date = new Date();
+			const rows = [];
+			const data = {
+				users: [],
+				msgs: 0,
+				files: [],
+				time: smarshHistory ? moment(date).diff(moment(smarshHistory.lastRan), 'minutes') : moment(date).diff(moment(room.ts), 'minutes'),
+				room: room.name ? `#${room.name}` : `Direct Message Between: ${room.usernames.join(' & ')}`
+			};
+
+			RocketChat.models.Messages.find(query).forEach((message) => {
+				rows.push(opentr);
+
+				//The timestamp
+				rows.push(open20td);
+				rows.push(moment(message.ts).tz('America/Los_Angeles').format('YYYY-MM-DD HH-mm-ss z'));
+				rows.push(closetd);
+
+				//The sender
+				rows.push(open20td);
+				const sender = RocketChat.models.Users.findOne({ _id: message.u._id });
+				if (data.users.indexOf(sender._id) === -1) {
+					data.users.push(sender._id);
+				}
+
+				//Get the user's email, can be nothing if it is an unconfigured bot account (like rocket.cat)
+				if (sender.emails && sender.emails[0] && sender.emails[0].address) {
+					rows.push(`${sender.name} &lt;${sender.emails[0].address}&gt;`);
+				} else {
+					rows.push(`${sender.name} &lt;${RocketChat.settings.get('Smarsh_MissingEmail_Email')}&gt;`);
+				}
+				rows.push(closetd);
+
+				//The message
+				rows.push(open60td);
+				data.msgs++;
+				if (message.t) {
+					const messageType = RocketChat.MessageTypes.getType(message);
+					if (messageType) {
+						rows.push(TAPi18n.__(messageType.message, messageType.data ? messageType.data(message) : '', 'en'));
+					} else {
+						rows.push(`${message.msg} (${message.t})`);
+					}
+				} else if (message.file) {
+					data.files.push(message.file._id);
+					rows.push(`${message.attachments[0].title} (${_getLink(message.attachments[0])})`);
+				} else if (message.attachments) {
+					const attaches = [];
+					_.each(message.attachments, function _loopThroughMessageAttachments(a) {
+						if (a.image_url) {
+							attaches.push(a.image_url);
+						}
+						//TODO: Verify other type of attachments which need to be handled that aren't file uploads and image urls
+						// } else {
+						// 	console.log(a);
+						// }
+					});
+
+					rows.push(`${message.msg} (${attaches.join(', ')})`);
+				} else {
+					rows.push(message.msg);
+				}
+				rows.push(closetd);
+
+				rows.push(closetr);
+			});
+
+			if (rows.length !== 0) {
+				const result = start + rows.join('') + end;
+
+				RocketChat.smarsh.History.upsert({ _id: room._id }, {
+					_id: room._id,
+					lastRan: date,
+					lastResult: result
+				});
+
+				RocketChat.smarsh.sendEmail({
+					body: result,
+					subject: `Rocket.Chat, ${data.users.length} Users, ${data.msgs} Messages, ${data.files.length} Files, ${data.time} Minutes, in ${data.room}`,
+					files: data.files
+				});
+			}
+		});
+	});
+};
diff --git a/packages/rocketchat-smarsh-connector/server/functions/sendEmail.js b/packages/rocketchat-smarsh-connector/server/functions/sendEmail.js
new file mode 100644
index 0000000000000000000000000000000000000000..d2a8c5fd4c70f0e29d977e9a55b75d20251231b7
--- /dev/null
+++ b/packages/rocketchat-smarsh-connector/server/functions/sendEmail.js
@@ -0,0 +1,32 @@
+/* globals UploadFS */
+//Expects the following details:
+// {
+// 	body: '<table>',
+// 	subject: 'Rocket.Chat, 17 Users, 24 Messages, 1 File, 799504 Minutes, in #random',
+//  files: ['i3nc9l3mn']
+// }
+
+RocketChat.smarsh.sendEmail = (data) => {
+	const attachments = [];
+
+	if (data.files.length > 0) {
+		_.each(data.files, (fileId) => {
+			const file = RocketChat.models.Uploads.findOneById(fileId);
+			if (file.store === 'rocketchat_uploads' || file.store === 'fileSystem') {
+				const rs = UploadFS.getStore(file.store).getReadStream(fileId, file);
+				attachments.push({
+					filename: file.name,
+					streamSource: rs
+				});
+			}
+		});
+	}
+
+	Email.send({
+		to: RocketChat.settings.get('Smarsh_Email'),
+		from: RocketChat.settings.get('From_Email'),
+		subject: data.subject,
+		html: data.body,
+		attachments: attachments
+	});
+};
diff --git a/packages/rocketchat-smarsh-connector/server/models/SmarshHistory.js b/packages/rocketchat-smarsh-connector/server/models/SmarshHistory.js
new file mode 100644
index 0000000000000000000000000000000000000000..8b28da7b963474d56ee1c2125a76aa3a59b7bd7d
--- /dev/null
+++ b/packages/rocketchat-smarsh-connector/server/models/SmarshHistory.js
@@ -0,0 +1,6 @@
+RocketChat.smarsh.History = new class extends RocketChat.models._Base {
+	constructor() {
+		super();
+		super._initModel('smarsh_history');
+	}
+};
diff --git a/packages/rocketchat-smarsh-connector/server/settings.js b/packages/rocketchat-smarsh-connector/server/settings.js
new file mode 100644
index 0000000000000000000000000000000000000000..239e8a8fb60c67cd4a4e11efbe7bf840de068ed3
--- /dev/null
+++ b/packages/rocketchat-smarsh-connector/server/settings.js
@@ -0,0 +1,46 @@
+RocketChat.settings.addGroup('Smarsh', function addSettings() {
+	this.add('Smarsh_Enabled', false, {
+		type: 'boolean',
+		i18nLabel: 'Smarsh_Enabled',
+		enableQuery: {
+			_id: 'From_Email',
+			value: {
+				$exists: 1,
+				$ne: ''
+			}
+		}
+	});
+	this.add('Smarsh_Email', '', {
+		type: 'string',
+		i18nLabel: 'Smarsh_Email',
+		placeholder: 'email@domain.com'
+	});
+	this.add('Smarsh_MissingEmail_Email', 'no-email@example.com', {
+		type: 'string',
+		i18nLabel: 'Smarsh_MissingEmail_Email',
+		placeholder: 'no-email@example.com'
+	});
+	this.add('Smarsh_Interval', 'every_30_minutes', {
+		type: 'select',
+		values: [{
+			key: 'every_30_seconds',
+			i18nLabel: 'every_30_seconds'
+		}, {
+			key: 'every_30_minutes',
+			i18nLabel: 'every_30_minutes'
+		}, {
+			key: 'every_1_hours',
+			i18nLabel: 'every_hour'
+		}, {
+			key: 'every_6_hours',
+			i18nLabel: 'every_six_hours'
+		}],
+		enableQuery: {
+			_id: 'From_Email',
+			value: {
+				$exists: 1,
+				$ne: ''
+			}
+		}
+	});
+});
diff --git a/packages/rocketchat-smarsh-connector/server/startup.js b/packages/rocketchat-smarsh-connector/server/startup.js
new file mode 100644
index 0000000000000000000000000000000000000000..a1d515a690d50ab7ecbcf151250d3b40221ddb29
--- /dev/null
+++ b/packages/rocketchat-smarsh-connector/server/startup.js
@@ -0,0 +1,27 @@
+/* globals SyncedCron */
+const smarshJobName = 'Smarsh EML Connector';
+
+const _addSmarshSyncedCronJob = _.debounce(Meteor.bindEnvironment(function __addSmarshSyncedCronJobDebounced() {
+	if (SyncedCron.nextScheduledAtDate(smarshJobName)) {
+		SyncedCron.remove(smarshJobName);
+	}
+
+	if (RocketChat.settings.get('Smarsh_Enabled') && RocketChat.settings.get('Smarsh_Email') !== '' && RocketChat.settings.get('From_Email') !== '') {
+		SyncedCron.add({
+			name: smarshJobName,
+			schedule: (parser) => parser.text(RocketChat.settings.get('Smarsh_Interval').replace(/_/g, ' ')),
+			job: RocketChat.smarsh.generateEml
+		});
+	}
+}), 500);
+
+Meteor.startup(() => {
+	Meteor.defer(() => {
+		_addSmarshSyncedCronJob();
+
+		RocketChat.settings.get('Smarsh_Interval', _addSmarshSyncedCronJob);
+		RocketChat.settings.get('Smarsh_Enabled', _addSmarshSyncedCronJob);
+		RocketChat.settings.get('Smarsh_Email', _addSmarshSyncedCronJob);
+		RocketChat.settings.get('From_Email', _addSmarshSyncedCronJob);
+	});
+});
diff --git a/packages/rocketchat-theme/assets/stylesheets/animation.css b/packages/rocketchat-theme/assets/stylesheets/animation.css
index 2a15ee23eae154d170128ffbe9d8b06cf9b4bc48..02a5251e73c73d70c23c1a8d82df6fcbd5155df1 100644
--- a/packages/rocketchat-theme/assets/stylesheets/animation.css
+++ b/packages/rocketchat-theme/assets/stylesheets/animation.css
@@ -8,6 +8,15 @@
   animation: spin 2s infinite linear;
   display: inline-block;
 }
+
+.animate-pulse {
+  -moz-animation: spin 1s infinite steps(8);
+  -o-animation: spin 1s infinite steps(8);
+  -webkit-animation: spin 1s infinite steps(8);
+  animation: spin 1s infinite steps(8);
+  display: inline-block;
+}
+
 @-moz-keyframes spin {
   0% {
     -moz-transform: rotate(0deg);
@@ -82,4 +91,4 @@
     -webkit-transform: rotate(359deg);
     transform: rotate(359deg);
   }
-}
\ No newline at end of file
+}
diff --git a/packages/rocketchat-theme/assets/stylesheets/base.less b/packages/rocketchat-theme/assets/stylesheets/base.less
index d7c3da5ce48d3be405525223f698be88e7ca057e..97b690bef415e359f761b611561725508181e455 100644
--- a/packages/rocketchat-theme/assets/stylesheets/base.less
+++ b/packages/rocketchat-theme/assets/stylesheets/base.less
@@ -491,7 +491,9 @@ input[type='text'],
 input[type='number'],
 input[type='email'],
 input[type='url'],
-input[type='password'] {
+input[type='password'],
+input[type='time']
+ {
 	-webkit-appearance: none;
 	-moz-appearance: none;
 	appearance: none;
@@ -534,6 +536,7 @@ form.inline {
 	input[type='email'],
 	input[type='url'],
 	input[type='password'],
+	input[type='time']
 	select {
 		width: auto;
 	}
@@ -577,18 +580,9 @@ textarea[disabled] {
 		font-size: 14px;
 		padding: 8px 8px;
 	}
-	> i {
-		visibility: hidden;
-		&:after {
-			content: " ";
-			visibility: visible;
-			background-image: url('images/logo/loading.gif');
-			background-position: center;
-			background-repeat: no-repeat;
-			display: block;
-			height: 40px;
-			margin-bottom: 12px;
-		}
+	.loading {
+		position: relative;
+		min-height: 60px;
 	}
 }
 
@@ -622,19 +616,16 @@ label.required:after {
 .toggle-favorite {}
 
 .loading {
-	background-image: url('images/loading.gif');
-	background-repeat: no-repeat;
-	background-position: 50%;
-	width: 100%;
-	height: 100%;
-	position: fixed;
 	top: 0;
+	right: 0;
+	bottom: 0;
 	left: 0;
-	&.inline {
-		position: relative;
-		min-height: 40px;
-		background-size: 24px 24px;
-	}
+	display: flex;
+	align-items: center;
+	position: absolute;
+	justify-content: center;
+	font-size: 2em;
+	color: #ccc;
 }
 
 .btn-loading {
@@ -659,12 +650,11 @@ label.required:after {
 	font-weight: 500;
 	font-size: 13px;
 	text-align: center;
-	margin: 4px;
 	text-transform: uppercase;
 	word-spacing: 0;
-	box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.125);
 	line-height: 16px;
 	position: relative;
+	border-radius: 4px;
 	span {
 		position: relative;
 		z-index: 2;
@@ -678,7 +668,6 @@ label.required:after {
 		height: 100%;
 		opacity: 0;
 		z-index: 1;
-		.transition(opacity .1s ease-out);
 	}
 	&:hover {
 		text-decoration: none;
@@ -704,19 +693,29 @@ label.required:after {
 			font-weight: 600;
 		}
 	}
-	&.facebook {}
-	&.twitter {}
-	&.google {}
-	&.github {}
-	&.gitlab {}
-	&.trello {}
-	&.meteor-developer {}
 	&.button-block {
 		display: block;
+		margin-bottom: 4px;
 		width: 100%;
 	}
 }
 
+.buttons-group {
+	display: -webkit-flex;
+	display: -moz-flex;
+	display: flex;
+	margin-bottom: 4px;
+	.button {
+		margin-left: 4px;
+	}
+	.button:first-child {
+		margin-left: 0px;
+		-webkit-flex-grow: 1;
+		-moz-flex-grow: 1;
+		flex-grow: 1;
+	}
+}
+
 .sec-header {
 	margin: 16px 0;
 	text-align: center;
@@ -1025,7 +1024,6 @@ label.required:after {
 			padding: 15px 12px;
 			line-height: 1;
 			text-decoration: none;
-			border-bottom: 1px solid;
 			&:nth-child(even) {}
 			&:hover {
 				text-decoration: none;
@@ -1574,7 +1572,7 @@ label.required:after {
 	max-width: 800px;
 	margin: 40px auto;
 	padding: 20px;
-	border-radius: 5px;
+	border-radius: 4px;
 	box-shadow: 1px 1px 4px rgba(0, 0, 0, .3);
 	.cms-page-close {
 		margin-bottom: 10px;
@@ -1698,10 +1696,10 @@ label.required:after {
 	}
 	.section {
 		border: 1px solid #ddd;
-		border-left: none;
+		border-radius: 4px;
 		background-color: #fff;
 		padding: 20px;
-		margin-bottom: 20px;
+		margin: 20px;
 		&.section-collapsed {
 			.section-content {
 				display: none;
@@ -1842,7 +1840,7 @@ label.required:after {
 			.section-content {
 				border: 1px solid;
 				padding: 20px;
-				border-radius: 5px;
+				border-radius: 4px;
 				.section-helper {
 					padding: 20px 20px 40px;
 					pre {
@@ -2075,6 +2073,7 @@ label.required:after {
 	height: 100%;
 	top: 0;
 	left: 0;
+	border-right: 1px solid;
 	.room-topic {
 		font-size: 14px;
 		opacity: 0.4;
@@ -2107,12 +2106,16 @@ label.required:after {
 		min-height: @footer-min-height;
 	}
 	.message-popup-results {
+		.loading {
+			display: none;
+		}
 		&.notready {
 			.message-popup-items {
-				background-image: url(images/logo/loading.gif);
-				background-repeat: no-repeat;
-				background-position: 50% 50%;
+				position: relative;
 				height: 100px;
+				.loading {
+					display: flex;
+				}
 			}
 			.popup-item {
 				display: none;
@@ -2691,6 +2694,14 @@ label.required:after {
 			}
 			button {
 				display: block;
+				color: #008ce3;
+				border: 0px;
+				min-height: 26px;
+				text-decoration: none;
+
+				&:hover {
+					color: #006db0;
+				}
 			}
 		}
 	}
@@ -2802,20 +2813,14 @@ body:not(.is-cordova) {
 	height: 100%;
 	top: 0;
 	right: 0;
-	background: #FCFCFC;
-	border-left: 1px solid #eaeaea;
 	z-index: 130;
 	.tab-button {
 		position: relative;
 		cursor: pointer;
-		padding: 10px 0;
-		background: #FCFCFC;
-		border-bottom: 1px solid #eaeaea;
 		text-align: center;
-		&:hover {
-			background: #EAEAEA;
+		button {
+			height: 38px;
 		}
-
 		.counter {
 			position: absolute;
 			background: #999;
@@ -2830,16 +2835,11 @@ body:not(.is-cordova) {
 			top: 4px;
 			text-align: center;
 		}
-
 		&.active {
-			background-color: #F4F4F4;
-			margin-left: -1px;
-			border-right: 3px solid #ff0000;
-
+			border-right: 3px solid;
 			button {
-				margin-left: 4px;
+				margin-left: 3px;
 			}
-
 			.counter {
 				margin-right: -3px;
 			}
@@ -2905,7 +2905,6 @@ body:not(.is-cordova) {
 
 // FLEX-TAB and FLEX-TAB views
 .flex-tab {
-	border-left: 1px solid;
 	overflow-x: visible;
 	position: fixed;
 	z-index: 110;
@@ -3117,6 +3116,11 @@ body:not(.is-cordova) {
 					color: #006db0;
 					text-decoration: underline;
 				}
+				p {
+					overflow: hidden;
+					text-overflow: ellipsis;
+					white-space: nowrap;
+				}
 			}
 		}
 		i {
@@ -3153,31 +3157,10 @@ body:not(.is-cordova) {
 	}
 	.thumb {
 		width: 100%;
-		height: 200px;
-
-		.transition(height .4s ease);
-
-		cursor: zoom-in;
-		cursor: -moz-zoom-in;
-		cursor: -webkit-zoom-in;
-
-		&.bigger {
-			height: 350px;
-			cursor: zoom-out;
-			cursor: -moz-zoom-out;
-			cursor: -webkit-zoom-out;
-		}
-
-		.avatar {
-			border-radius: 0;
-
-			.avatar-image {
-				border-radius: 0;
-			}
-		}
+		height: 350px;
+		padding: 20px;
 	}
 	nav {
-		margin-left: -4px;
 		padding: 0 20px;
 		.back {
 			float: right;
@@ -3692,8 +3675,8 @@ body:not(.is-cordova) {
 	max-width: 520px;
 	padding: 20px;
 	margin: 20px auto;
-	box-shadow: 0 0 6px 10px rgba(0, 0, 0, 0.1);
-	border-radius: 2px;
+	box-shadow: 0 1px 1px 0 rgba(0,0,0,0.2),0 2px 10px 0 rgba(0,0,0,0.16);
+	border-radius: 4px;
 	position: relative;
 	z-index: 1;
 	header {
@@ -3718,29 +3701,13 @@ body:not(.is-cordova) {
 	img {
 		width: 200px;
 	}
-	a {
-		margin: 4px 0;
-		display: inline-block;
-		&:active {}
-		&:hover {}
-	}
 	.options {
 		display: none;
 		width: 100%;
 		font-size: 10px;
 	}
-	.submit {
-		margin: 12px 0;
-	}
-	.remember {
-		float: left;
-	}
-	.remember input {
-		margin-right: 4px;
-	}
-	.forgot {
-		float: right;
-		line-height: 20px;
+	.submit, .register, .forgot-password, .back-to-login {
+		margin-top: 12px;
 	}
 	.input-text {
 		margin: 0 0 14px 0;
@@ -3763,6 +3730,7 @@ body:not(.is-cordova) {
 			box-shadow: 0 0 0;
 			border-width: 0;
 			position: relative;
+			margin-top: 14px;
 			padding: 4px 8px;
 			font-size: 18px;
 			border-bottom: 1px solid;
@@ -3783,19 +3751,29 @@ body:not(.is-cordova) {
 		}
 		.select-arrow {
 			position: absolute;
-			top: 10px;
+			bottom: 11px;
 			right: 0px;
 			color: #a9a9a9;
 		}
-		input:-webkit-autofill {}
-		input:-webkit-autofill {
-			-webkit-box-shadow: 0 0 0px 1000px #f4f4f4 inset;
+		label {
+			position: absolute;
+			top: 20px;
+			left: 8px;
+			display: block;
+			font-size: 18px;
+			text-align: left;
+			color: #a9a9a9;
+			transition: all 0.3s;
+			z-index: 100;
+		}
+		&.focus label {
+			top: 0;
+			font-size: 12px;
 		}
-
 		.input-error {
 			text-align: left;
 			color: #b40202;
-			padding-left: 4px;
+			padding-left: 8px;
 			font-weight: bold;
 			font-size: 14px;
 		}
@@ -3803,26 +3781,10 @@ body:not(.is-cordova) {
 }
 
 .social-login {
-	text-align: center;
-	position: relative;
-	z-index: 1;
-	display: -webkit-flex;
-	display: flex;
-	flex-wrap: wrap;
-	-webkit-flex-wrap: wrap;
 	margin-bottom: 20px;
-	h3 {
-		&:extend(.rocket-h3);
-		margin-top: 0;
-		margin-bottom: 12px;
-	}
 	.button {
-		border-radius: 2px;
-		min-height: 40px;
-		line-height: 20px;
+		line-height: 22px;
 		font-size: 18px;
-		margin: 2px;
-		padding: 0;
 		-webkit-flex-grow: 1;
 		flex-grow: 1;
 	}
@@ -4373,17 +4335,6 @@ body:not(.is-cordova) {
 	}
 }
 
-.group-call-buttons {
-	display: -webkit-flex;
-	display: -moz-flex;
-	display: flex;
-	button:first-child {
-		-webkit-flex-grow: 1;
-		-moz-flex-grow: 1;
-		flex-grow: 1;
-	}
-}
-
 .alert-icon {
 	font-size: 80px;
 	display: block;
@@ -4522,72 +4473,6 @@ body:not(.is-cordova) {
 	margin-top: 1em;
 }
 
-.page-loading {
-	position: absolute;
-	top: 0;
-	left: 0;
-	right: 0;
-	bottom: 0;
-	background-color: rgba(0, 0, 0, .5);
-	z-index: 1000;
-	display: flex;
-	align-items: center;
-	justify-content: center;
-	.spinner {
-		margin: 10px auto;
-		width: 50px;
-		height: 40px;
-		text-align: center;
-		font-size: 10px;
-	}
-	.spinner > div {
-		background-color: #fff;
-		height: 100%;
-		width: 6px;
-		display: inline-block;
-		-webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out;
-		animation: sk-stretchdelay 1.2s infinite ease-in-out;
-	}
-	.spinner .rect2 {
-		-webkit-animation-delay: -1.1s;
-		animation-delay: -1.1s;
-	}
-	.spinner .rect3 {
-		-webkit-animation-delay: -1.0s;
-		animation-delay: -1.0s;
-	}
-	.spinner .rect4 {
-		-webkit-animation-delay: -0.9s;
-		animation-delay: -0.9s;
-	}
-	.spinner .rect5 {
-		-webkit-animation-delay: -0.8s;
-		animation-delay: -0.8s;
-	}
-	@-webkit-keyframes sk-stretchdelay {
-		0%,
-		40%,
-		100% {
-			-webkit-transform: scaleY(0.4)
-		}
-		20% {
-			-webkit-transform: scaleY(1.0)
-		}
-	}
-	@keyframes sk-stretchdelay {
-		0%,
-		40%,
-		100% {
-			transform: scaleY(0.4);
-			-webkit-transform: scaleY(0.4);
-		}
-		20% {
-			transform: scaleY(1.0);
-			-webkit-transform: scaleY(1.0);
-		}
-	}
-}
-
 .code-error-box {
 	.title {
 		background: red;
diff --git a/packages/rocketchat-theme/assets/stylesheets/utils/_colors.import.less b/packages/rocketchat-theme/assets/stylesheets/utils/_colors.import.less
index 2f61a2d9c08fb3586cef7d0112f09280941849c6..b22a76c5c9b5a9e4b6baa29a60b3517bc4ca0b98 100755
--- a/packages/rocketchat-theme/assets/stylesheets/utils/_colors.import.less
+++ b/packages/rocketchat-theme/assets/stylesheets/utils/_colors.import.less
@@ -393,7 +393,6 @@ a.github-fork {
 		}
 		button, a {
 			color: fade( @quaternary-font-color, 50%);
-			border-bottom-color: darken(@primary-background-color, 2%);
 			&:hover {
 				background-color: darken(@primary-background-color, 2%);
 				color: fade( @quaternary-font-color, 75%);
@@ -689,6 +688,7 @@ a.github-fork {
 
 // change to page-messages
 .messages-container {
+	border-right-color: @tertiary-background-color;
 	.edit-room-title {
 		color: @secondary-font-color;
 		&:hover {
@@ -832,7 +832,6 @@ a.github-fork {
 // FLEX-TAB and FLEX-TAB views
 .flex-tab {
 	background-color: @secondary-background-color;
-	border-left-color: @tertiary-background-color;
 	.control {
 		background-color: @secondary-background-color;
 		&:before {
@@ -1128,15 +1127,6 @@ a.github-fork {
 			color: #b40202;
 		}
 	}
-	a {
-		color: @primary-background-color;
-		&:active {
-			color: @primary-background-color;
-		}
-		&:hover {
-			color: darken(@primary-background-color, 10%);
-		}
-	}
 	.input-text {
 		input, select {
 			background-color: transparent;
@@ -1160,10 +1150,9 @@ a.github-fork {
 			}
 		}
 		input:-webkit-autofill {
-			color: @content-background-color !important;
-		}
-		input:-webkit-autofill {
-			background-color: transparent !important;
+			color: @primary-font-color !important;
+			background-color: #FAFAFA !important;
+			-webkit-box-shadow: 0 0 0px 1000px #FAFAFA inset;
 		}
 	}
 }
@@ -1241,10 +1230,18 @@ a.github-fork {
 }
 
 .flex-tab-bar {
+	background: #FCFCFC;
 	.tab-button {
+		&:hover {
+			background: #EAEAEA;
+		}
 		&.red {
 			color: red;
 		}
+		&.active {
+			background-color: @secondary-background-color;
+			border-color: #ff0000;
+		}
 		&.attention {
 			animation-duration: 1000ms;
 			animation-name: blink;
diff --git a/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.coffee b/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.coffee
index e48436f960bd462964bd9ca91e45dcfd062494df..063582c8e77bb4734de2b94896e86c72f6dae57f 100644
--- a/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.coffee
+++ b/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.coffee
@@ -32,6 +32,16 @@ Template.adminRoomInfo.helpers
 	canDeleteRoom: ->
 		roomType = ChatRoom.findOne(@rid, { fields: { t: 1 }})?.t
 		return roomType? and RocketChat.authz.hasAtLeastOnePermission("delete-#{roomType}")
+	readOnly: ->
+		room = ChatRoom.findOne(@rid, { fields: { ro: 1 }})
+		return room?.ro
+	readOnlyDescription: ->
+		room = ChatRoom.findOne(@rid, { fields: { ro: 1 }}) 
+		readOnly = room?.ro
+		if readOnly is true
+			return t('True')
+		else
+			return t('False')
 
 Template.adminRoomInfo.events
 	'click .delete': ->
@@ -146,4 +156,8 @@ Template.adminRoomInfo.onCreated ->
 							return handleError(err) if err
 							toastr.success TAPi18n.__ 'Room_unarchived'
 							RocketChat.callbacks.run 'unarchiveRoom', ChatRoom.findOne(rid)
+			when 'readOnly'
+				Meteor.call 'saveRoomSettings', rid, 'readOnly', @$('input[name=readOnly]:checked').val() is 'true', (err, result) ->
+					return handleError err if err
+					toastr.success TAPi18n.__ 'Read_only_changed_successfully'
 		@editing.set()
diff --git a/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.html b/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.html
index 2240950882f6968b929160b89eaa474cb34dd1df..03ea571241394f43ff0d7cc642a7658749349cab 100644
--- a/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.html
+++ b/packages/rocketchat-ui-admin/admin/rooms/adminRoomInfo.html
@@ -69,7 +69,7 @@
 										<button type="button" class="button secondary cancel">{{_ "Cancel"}}</button>
 										<button type="button" class="button primary save">{{_ "Save"}}</button>
 									{{else}}
-										<span>{{#if readOnlyDescription}}{{_ "True"}}{{else}}{{_ "False"}}{{/if}}{{#if canEdit}} <i class="icon-pencil" data-edit="readOnly"></i>{{/if}}</span>
+										<span>{{readOnlyDescription}}{{#if canEdit}} <i class="icon-pencil" data-edit="readOnly"></i>{{/if}}</span>
 									{{/if}}
 								</div>
 							</li>
diff --git a/packages/rocketchat-ui-admin/publications/adminRooms.js b/packages/rocketchat-ui-admin/publications/adminRooms.js
index 1cc49f6ba371ce8c91cb41e0c884580562ffad14..950b70213f57dd586eeabcb7ec758ae3c7e70e21 100644
--- a/packages/rocketchat-ui-admin/publications/adminRooms.js
+++ b/packages/rocketchat-ui-admin/publications/adminRooms.js
@@ -17,6 +17,7 @@ Meteor.publish('adminRooms', function(filter, types, limit) {
 			u: 1,
 			usernames: 1,
 			muted: 1,
+			ro: 1,
 			default: 1,
 			topic: 1,
 			msgs: 1,
diff --git a/packages/rocketchat-ui-login/login/form.coffee b/packages/rocketchat-ui-login/login/form.coffee
index fb6912ea14363d515eb7ac243fbac0ac21db6564..ab1061a66b307ad0a3c608d843db25777ee6e387 100644
--- a/packages/rocketchat-ui-login/login/form.coffee
+++ b/packages/rocketchat-ui-login/login/form.coffee
@@ -14,7 +14,7 @@ Template.loginForm.helpers
 	btnLoginSave: ->
 		switch Template.instance().state.get()
 			when 'register'
-				return t('Submit')
+				return t('Register')
 			when 'login'
 				return t('Login')
 			when 'email-verification'
@@ -147,6 +147,13 @@ Template.loginForm.events
 
 		OnePassword.findLoginForUrl(succesCallback, errorCallback, Meteor.absoluteUrl())
 
+	'focus .input-text input': (event) ->
+		$(event.currentTarget).parents('.input-text').addClass('focus')
+
+	'blur .input-text input': (event) ->
+		if event.currentTarget.value is ''
+			$(event.currentTarget).parents('.input-text').removeClass('focus')
+
 
 Template.loginForm.onCreated ->
 	instance = @
diff --git a/packages/rocketchat-ui-login/login/form.html b/packages/rocketchat-ui-login/login/form.html
index c1bc71318718a8bb81123e075697a2d79dc9f84f..eea75f0a01419b67681fb6869eb1ced1bad5a6ca 100644
--- a/packages/rocketchat-ui-login/login/form.html
+++ b/packages/rocketchat-ui-login/login/form.html
@@ -24,40 +24,45 @@
 					<div class="fields">
 						{{#if state 'login'}}
 							<div class='input-text active'>
-								<input type="text" name='emailOrUsername' placeholder='{{emailOrUsernamePlaceholder}}' autocapitalize="off" autocorrect="off" />
+								<label for="emailOrUsername">{{emailOrUsernamePlaceholder}}</label>
+								<input type="text" name='emailOrUsername' id="emailOrUsername" autocapitalize="off" autocorrect="off" />
 								{{#if hasOnePassword}}
 									<div class="one-passsword"></div>
 								{{/if}}
 								<div class="input-error"></div>
 							</div>
 							<div class='input-text active'>
-								<input type="password" name='pass' placeholder='{{passwordPlaceholder}}' />
+								<label for="pass">{{passwordPlaceholder}}</label>
+								<input type="password" name='pass' id="pass" />
 								<div class="input-error"></div>
 							</div>
 						{{/if}}
 
 						{{#if state 'register'}}
 							<div class='input-text active'>
-								<input type="text" name='name' placeholder='{{namePlaceholder}}' dir="auto" />
+								<label for="name">{{namePlaceholder}}</label>
+								<input type="text" name='name' id="name" dir="auto" />
 								<div class="input-error"></div>
 							</div>
 							<div class='input-text active'>
-								<input type="email" name='email' placeholder='{{_ "Email"}}' />
+								<label for="email">{{_ "Email"}}</label>
+								<input type="email" name='email' id="email" />
 								<div class="input-error"></div>
 							</div>
 
 							{{#each customFields}}
 								{{#if $eq field.type 'select'}}
-									<div class='input-text active'>
+									<div class='input-text active focus'>
+										<label for="{{fieldName}}">{{_ fieldName}}</label>
 										<div class="select-arrow">
 											<i class="icon-down-open"></i>
 										</div>
 										<select name="{{fieldName}}" data-customfield="true">
 											{{#each field.options}}
 												{{#if $eq . ../field.defaultValue}}
-													<option value="{{.}}" selected>{{_ ../fieldName}}: {{_ .}}</option>
+													<option value="{{.}}" selected>{{_ .}}</option>
 												{{else}}
-													<option value="{{.}}">{{_ ../fieldName}}: {{_ .}}</option>
+													<option value="{{.}}">{{_ .}}</option>
 												{{/if}}
 											{{/each}}
 										</select>
@@ -67,20 +72,23 @@
 
 								{{#if $eq field.type 'text'}}
 									<div class='input-text active'>
-										<input type="text" name="{{fieldName}}" data-customfield="true" placeholder="{{_ fieldName}}" value="{{field.defaultValue}}" maxlength="{{field.maxLength}}" />
+										<label for="{{fieldName}}">{{_ fieldName}}</label>
+										<input type="text" name="{{fieldName}}" id="{{fieldName}}" data-customfield="true" value="{{field.defaultValue}}" maxlength="{{field.maxLength}}" />
 										<div class="input-error"></div>
 									</div>
 								{{/if}}
 							{{/each}}
 
 							<div class='input-text active'>
-								<input type="password" name='pass' placeholder='{{passwordPlaceholder}}' />
+								<label for="pass">{{passwordPlaceholder}}</label>
+								<input type="password" name='pass' id="pass" />
 								<div class="input-error"></div>
 							</div>
 
 							{{#if requirePasswordConfirmation}}
 								<div class='input-text active'>
-									<input type="password" name='confirm-pass' placeholder='{{_ "Confirm_password"}}' />
+									<label for="confirm-pass">{{_ "Confirm_password"}}</label>
+									<input type="password" name='confirm-pass' id="confirm-pass" />
 									<div class="input-error"></div>
 								</div>
 							{{/if}}
@@ -88,7 +96,8 @@
 
 						{{#if state 'forgot-password' 'email-verification'}}
 							<div class='input-text active'>
-								<input type="email" name='email' placeholder='{{_ "Email"}}' />
+								<label for="email">{{_ "Email"}}</label>
+								<input type="email" name='email' id="email" />
 							</div>
 						{{/if}}
 					</div>
@@ -99,8 +108,8 @@
 
 					{{#if state 'login'}}
 						{{#if registrationAllowed}}
-							<div class="register">
-								<button type="button">{{_ 'Register'}}</button>
+							<div>
+								<button type="button" class="register">{{_ 'Register'}}</button>
 							</div>
 						{{else}}
 							{{#if linkReplacementText}}
@@ -109,8 +118,8 @@
 						{{/if}}
 
 						{{#if passwordResetAllowed}}
-							<div class="forgot-password">
-								<button type="button">{{_ 'Forgot_password'}}</button>
+							<div>
+								<button type="button" class="forgot-password">{{_ 'Forgot_password'}}</button>
 							</div>
 						{{/if}}
 					{{/if}}
@@ -118,8 +127,8 @@
 			{{/if}}
 
 			{{#unless state 'login'}}
-				<div class="back-to-login">
-					<button type="button">{{_ 'Back_to_login'}}</button>
+				<div>
+					<button type="button" class="back-to-login">{{_ 'Back_to_login'}}</button>
 				</div>
 			{{/unless}}
 		</form>
diff --git a/packages/rocketchat-ui-login/login/services.html b/packages/rocketchat-ui-login/login/services.html
index c71ee6728e5d3e0cfd8323865881ba6989f3922c..7eb0c3dfebbc747a2a6b2a1b6f0d1c3f42c5a67c 100644
--- a/packages/rocketchat-ui-login/login/services.html
+++ b/packages/rocketchat-ui-login/login/services.html
@@ -1,6 +1,6 @@
 <template name='loginServices'>
 	{{#if loginService.length}}
-		<div class="social-login">
+		<div class="social-login buttons-group">
 			{{#each loginService}}
 				<button type="button" class="button external-login {{service.service}}" title="{{displayName}}" style="{{#if service.buttonColor}}background-color:{{service.buttonColor}};{{/if}}{{#if service.buttonLabelColor}}color:{{service.buttonLabelColor}};{{/if}}"><i class="icon-{{icon}} service-icon"></i><i class="icon-spin animate-spin loading-icon hidden"></i><span>{{service.buttonLabelText}}</span></button>
 			{{/each}}
diff --git a/packages/rocketchat-ui-login/reset-password/resetPassword.html b/packages/rocketchat-ui-login/reset-password/resetPassword.html
index f88c29023b967ab9a5104bd86445882de50d3e01..8592692eb7532b06ada3286046af2fbce03be70e 100644
--- a/packages/rocketchat-ui-login/reset-password/resetPassword.html
+++ b/packages/rocketchat-ui-login/reset-password/resetPassword.html
@@ -13,7 +13,8 @@
 			{{/if}}
 			</header>
 			<div class="input-text active">
-				<input type="password" name="newPassword" placeholder="{{_ "Type_your_new_password"}}" dir="auto" />
+				<label for="newPassword">{{_ "Type_your_new_password"}}</label>
+				<input type="password" name="newPassword" id="newPassword" dir="auto" />
 			</div>
 			<div class="submit">
 				<button data-loading-text="{{_ "Please_wait"}}..."  class="button primary resetpass"><span>{{_ "Reset"}}</span></button>
diff --git a/packages/rocketchat-ui-login/reset-password/resetPassword.js b/packages/rocketchat-ui-login/reset-password/resetPassword.js
index 3294916a2c8e301338319f758bda9835c1667156..b3222a2340f9ae05baab85489487ab3afe02829e 100644
--- a/packages/rocketchat-ui-login/reset-password/resetPassword.js
+++ b/packages/rocketchat-ui-login/reset-password/resetPassword.js
@@ -14,6 +14,14 @@ Template.resetPassword.helpers({
 });
 
 Template.resetPassword.events({
+	'focus .input-text input': function(event) {
+		$(event.currentTarget).parents('.input-text').addClass('focus');
+	},
+	'blur .input-text input': function(event) {
+		if (event.currentTarget.value === '') {
+			$(event.currentTarget).parents('.input-text').removeClass('focus');
+		}
+	},
 	'submit #login-card': function(event, instance) {
 		event.preventDefault();
 
diff --git a/packages/rocketchat-ui-login/username/username.coffee b/packages/rocketchat-ui-login/username/username.coffee
index 2cf00abd556576fe956e436b04bf22d2b2ffb9f9..3517e9fafcadeb14708dc12f4a8d09999a6b02e0 100644
--- a/packages/rocketchat-ui-login/username/username.coffee
+++ b/packages/rocketchat-ui-login/username/username.coffee
@@ -14,6 +14,13 @@ Template.username.helpers
 		return Template.instance().username.get()
 
 Template.username.events
+	'focus .input-text input': (event) ->
+		$(event.currentTarget).parents('.input-text').addClass('focus')
+
+	'blur .input-text input': (event) ->
+		if event.currentTarget.value is ''
+			$(event.currentTarget).parents('.input-text').removeClass('focus')
+
 	'submit #login-card': (event, instance) ->
 		event.preventDefault()
 
diff --git a/packages/rocketchat-ui-login/username/username.html b/packages/rocketchat-ui-login/username/username.html
index 5c5af5dc37c1c7e632123f020c860772ab4eebe8..4f12d49f0d30636fc0807c594f1c0937c02e63de 100644
--- a/packages/rocketchat-ui-login/username/username.html
+++ b/packages/rocketchat-ui-login/username/username.html
@@ -25,7 +25,8 @@
 					<div class='input-text active'>
 						{{#if username.ready}}
 							<span>
-								<input type="text" name='username' value="{{username.username}}" placeholder='{{_ "Username"}}' dir="auto"/>
+								<label for="username">{{_ "Username"}}</label>
+								<input type="text" name='username' id="username" value="{{username.username}}" dir="auto"/>
 							</span>
 							<i></i>
 						{{else}}
diff --git a/packages/rocketchat-ui-master/master/loading.html b/packages/rocketchat-ui-master/master/loading.html
index 91f1bf7aee0c47cb816c5b42e73adb16955bce5e..6ca8ffd53eff54a0c67edcbba56c8c53b068915f 100644
--- a/packages/rocketchat-ui-master/master/loading.html
+++ b/packages/rocketchat-ui-master/master/loading.html
@@ -1,24 +1,5 @@
 <template name="loading">
-	<svg class="rocket-loader" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="200px" height="200px" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
-		<g>
-			<path class="outer" fill="#CC3333" d="M188.146,99.881c0-8.852-2.647-17.341-7.873-25.232c-4.691-7.083-11.263-13.354-19.533-18.637
-				c-15.967-10.199-36.953-15.817-59.089-15.817c-7.395,0-14.682,0.625-21.751,1.863c-4.387-4.105-9.521-7.798-14.953-10.72
-				c-29.024-14.066-53.094-0.331-53.094-0.331s22.379,18.383,18.739,34.499c-10.012,9.931-15.438,21.906-15.438,34.375
-				c0,0.04,0.002,0.08,0.003,0.119c-0.001,0.04-0.003,0.08-0.003,0.119c0,12.469,5.426,24.443,15.438,34.375
-				c3.64,16.114-18.739,34.498-18.739,34.498s24.069,13.735,53.094-0.33c5.433-2.922,10.566-6.615,14.953-10.721
-				c7.069,1.239,14.356,1.863,21.751,1.863c22.136,0,43.122-5.617,59.089-15.816c8.271-5.282,14.842-11.554,19.533-18.637
-				c5.226-7.892,7.873-16.38,7.873-25.232c0-0.04-0.002-0.08-0.002-0.119S188.146,99.92,188.146,99.881z"/>
-			<path class="inner" fill="#FFFFFF" d="M101.686,51.726c41.843,0,75.765,21.667,75.765,48.395c0,26.728-33.922,48.396-75.765,48.396
-				c-9.317,0-18.239-1.076-26.483-3.042c-8.378,10.08-26.809,24.092-44.713,19.562c5.823-6.255,14.452-16.825,12.604-34.233
-				c-10.731-8.351-17.173-19.037-17.173-30.683C25.921,73.393,59.842,51.726,101.686,51.726"/>
-			<g>
-				<circle fill="#CC3333" cx="136.68" cy="100.121" r="10.063"/>
-				<circle fill="#CC3333" cx="101.685" cy="100.121" r="10.064"/>
-				<circle fill="#CC3333" cx="66.691" cy="100.121" r="10.064"/>
-			</g>
-			<path class="inner" fill="#CCCCCC" d="M101.686,142.149c-9.317,0-18.239-0.933-26.483-2.636c-7.397,7.713-22.634,18.081-38.425,17.699
-				c-2.079,3.153-4.34,5.732-6.288,7.824c17.904,4.53,36.335-9.481,44.713-19.562c8.244,1.966,17.166,3.042,26.483,3.042
-				c41.507,0,75.214-21.324,75.752-47.755C176.899,123.67,143.192,142.149,101.686,142.149z"/>
-		</g>
-	</svg>
+	<div class="loading">
+		<i class="icon-spinner animate-pulse"></i>
+	</div>
 </template>
diff --git a/packages/rocketchat-ui-master/master/main.html b/packages/rocketchat-ui-master/master/main.html
index a5f089eb6bdac7441199b850316c3a4470991387..357f60a39079c9f4a63114fcd88c4dd533e74fb9 100644
--- a/packages/rocketchat-ui-master/master/main.html
+++ b/packages/rocketchat-ui-master/master/main.html
@@ -86,6 +86,6 @@
 			<script>{{{CustomScriptLoggedIn}}}</script>
 		{{/unless}}
 	{{else}}
-		{{> pageLoading}}
+		{{> loading}}
 	{{/if}}
 </template>
diff --git a/packages/rocketchat-ui-master/master/pageLoading.html b/packages/rocketchat-ui-master/master/pageLoading.html
deleted file mode 100644
index 58baf29401aabbd88689a56691b4ad92586709f5..0000000000000000000000000000000000000000
--- a/packages/rocketchat-ui-master/master/pageLoading.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<template name="pageLoading">
-	<div class="page-loading">
-		<div class="spinner">
-			<div class="rect1"></div>
-			<div class="rect2"></div>
-			<div class="rect3"></div>
-			<div class="rect4"></div>
-			<div class="rect5"></div>
-		</div>
-	</div>
-</template>
diff --git a/packages/rocketchat-ui-master/package.js b/packages/rocketchat-ui-master/package.js
index 587555fc5dafbf27929c1bfb916330f07934dff5..ce4e67300acc9b52e86b091360965b501c85a709 100644
--- a/packages/rocketchat-ui-master/package.js
+++ b/packages/rocketchat-ui-master/package.js
@@ -26,7 +26,6 @@ Package.onUse(function(api) {
 
 	api.addFiles('master/main.html', 'client');
 	api.addFiles('master/loading.html', 'client');
-	api.addFiles('master/pageLoading.html', 'client');
 	api.addFiles('master/error.html', 'client');
 	api.addFiles('master/logoLayout.html', 'client');
 
diff --git a/packages/rocketchat-ui-master/server/inject.js b/packages/rocketchat-ui-master/server/inject.js
index 4f076798f9f7f9980f8c62bfff6b3e305a6b27b9..8ef623f7998a4ad8f6ad857f8c90e32973132a8a 100644
--- a/packages/rocketchat-ui-master/server/inject.js
+++ b/packages/rocketchat-ui-master/server/inject.js
@@ -2,12 +2,8 @@
 
 Inject.rawBody('page-loading', `
 	<div id="initial-page-loading" class="page-loading">
-		<div class="spinner">
-			<div class="rect1"></div>
-			<div class="rect2"></div>
-			<div class="rect3"></div>
-			<div class="rect4"></div>
-			<div class="rect5"></div>
+		<div class="loading">
+			<i class="icon-spinner animate-pulse"></i>
 		</div>
 	</div>`
 );
diff --git a/packages/rocketchat-ui-message/message/messageBox.coffee b/packages/rocketchat-ui-message/message/messageBox.coffee
index 816114c87219ee4997e8f16e01c2ec1116dcb254..0206c73e38511d1295d9be1a0adff1ad05a1994a 100644
--- a/packages/rocketchat-ui-message/message/messageBox.coffee
+++ b/packages/rocketchat-ui-message/message/messageBox.coffee
@@ -26,6 +26,8 @@ Template.messageBox.helpers
 		return RocketChat.settings.get('Message_ShowFormattingTips') and (RocketChat.Markdown or RocketChat.MarkdownCode or katexSyntax())
 	canJoin: ->
 		return RocketChat.roomTypes.verifyShowJoinLink @_id
+	joinCodeRequired: ->
+		return Session.get('roomData' + this._id)?.joinCodeRequired
 	subscribed: ->
 		return RocketChat.roomTypes.verifyCanSendMessage @_id
 	allowedToSend: ->
@@ -86,7 +88,15 @@ Template.messageBox.events
 	'click .join': (event) ->
 		event.stopPropagation()
 		event.preventDefault()
-		Meteor.call 'joinRoom', @_id
+		Meteor.call 'joinRoom', @_id, Template.instance().$('[name=joinCode]').val(), (err) ->
+			if err?
+				toastr.error t(err.reason)
+
+			if RocketChat.authz.hasAllPermission('preview-c-room') is false and RoomHistoryManager.getRoom(@_id).loaded is 0
+				RoomManager.getOpenedRoomByRid(@_id).streamActive = false
+				RoomManager.getOpenedRoomByRid(@_id).ready = false
+				RoomHistoryManager.getRoom(@_id).loaded = undefined
+				RoomManager.computation.invalidate()
 
 	'focus .input-message': (event) ->
 		KonchatNotification.removeRoomNotification @_id
@@ -105,7 +115,12 @@ Template.messageBox.events
 		chatMessages[@_id].keyup(@_id, event, instance)
 		instance.isMessageFieldEmpty.set(chatMessages[@_id].isEmpty())
 
-	'paste .input-message': (e) ->
+	'paste .input-message': (e, instance) ->
+		Meteor.setTimeout ->
+			input = instance.find('.input-message')
+			input.updateAutogrow?()
+		, 50
+
 		if not e.originalEvent.clipboardData?
 			return
 
diff --git a/packages/rocketchat-ui-message/message/messageBox.html b/packages/rocketchat-ui-message/message/messageBox.html
index ba633eb6dfe8846cdf4ddf57e34512878891a4b3..0312fc164538215ea6c8659e712bd8a06d9e6e5f 100644
--- a/packages/rocketchat-ui-message/message/messageBox.html
+++ b/packages/rocketchat-ui-message/message/messageBox.html
@@ -110,6 +110,9 @@
 			{{#if canJoin}}
 			<div>
 				{{{_ "you_are_in_preview_mode_of" room_name=roomName}}}
+				{{#if joinCodeRequired}}
+					<input type="text" name="joinCode" placeholder="{{_ 'Code'}}" style="width: 100px">
+				{{/if}}
 				<button class="button join"><span><i class="icon-login"></i> {{_ "join"}}</span></button>
 			</div>
 			{{/if}}
diff --git a/packages/rocketchat-ui-message/message/popup/messagePopup.html b/packages/rocketchat-ui-message/message/popup/messagePopup.html
index 98d57a2d19e75d40fa0591350ce8c668d9ca099a..de4d729b63ab4c901d83b0c238e779dbff587e5a 100644
--- a/packages/rocketchat-ui-message/message/popup/messagePopup.html
+++ b/packages/rocketchat-ui-message/message/popup/messagePopup.html
@@ -6,6 +6,7 @@
 					{{title}}
 				</div>
 				<div class="message-popup-items">
+					{{> loading}}
 					{{#each data}}
 						<div class="popup-item" data-id="{{_id}}">
 							{{> Template.dynamic template=../template}}
diff --git a/packages/rocketchat-ui/lib/RoomHistoryManager.coffee b/packages/rocketchat-ui/lib/RoomHistoryManager.coffee
index 70089fc4113b91166fa09449bd1312732c5eabc4..7ee6c25f28eae6fea0921ba6504147f8b3ba5375 100644
--- a/packages/rocketchat-ui/lib/RoomHistoryManager.coffee
+++ b/packages/rocketchat-ui/lib/RoomHistoryManager.coffee
@@ -11,7 +11,7 @@
 				isLoading: new ReactiveVar false
 				unreadNotLoaded: new ReactiveVar 0
 				firstUnread: new ReactiveVar
-				loaded: 0
+				loaded: undefined
 
 		return histories[rid]
 
@@ -63,7 +63,9 @@
 				RoomManager.updateMentionsMarksOfRoom typeName
 
 			room.isLoading.set false
-			room.loaded += result?.messages?.length
+			room.loaded ?= 0
+			if result?.messages?.length?
+				room.loaded += result.messages.length
 			if result?.messages?.length < limit
 				room.hasMore.set false
 
@@ -102,7 +104,9 @@
 					RoomManager.updateMentionsMarksOfRoom typeName
 
 				room.isLoading.set false
-				room.loaded += result.messages.length
+				room.loaded ?= 0
+				if result.messages.length?
+					room.loaded += result.messages.length
 				if result.messages.length < limit
 					room.hasMoreNext.set false
 
@@ -170,7 +174,9 @@
 					setTimeout ->
 						msgElement.removeClass('highlight')
 					, 500
-				room.loaded += result.messages.length
+				room.loaded ?= 0
+				if result.messages.length?
+					room.loaded += result.messages.length
 				room.hasMore.set result.moreBefore
 				room.hasMoreNext.set result.moreAfter
 
@@ -187,7 +193,7 @@
 	getMoreIfIsEmpty = (rid) ->
 		room = getRoom rid
 
-		if room.loaded is 0
+		if room.loaded is undefined
 			getMore rid
 
 
diff --git a/packages/rocketchat-ui/lib/RoomManager.coffee b/packages/rocketchat-ui/lib/RoomManager.coffee
index a29280f5d854873502826912c8057d09a9abec02..a1c45b28a14b61b7a5b64b7d5a6dbad46a447898 100644
--- a/packages/rocketchat-ui/lib/RoomManager.coffee
+++ b/packages/rocketchat-ui/lib/RoomManager.coffee
@@ -42,7 +42,7 @@ Tracker.autorun ->
 	if Meteor.userId()
 		RocketChat.Notifications.onUser 'message', (msg) ->
 			msg.u =
-				username: 'rocketbot'
+				username: 'rocket.cat'
 			msg.private = true
 
 			ChatMessage.upsert { _id: msg._id }, msg
@@ -245,6 +245,7 @@ Tracker.autorun ->
 	onlineUsers: onlineUsers
 	updateMentionsMarksOfRoom: updateMentionsMarksOfRoom
 	getOpenedRoomByRid: getOpenedRoomByRid
+	computation: computation
 
 
 RocketChat.callbacks.add 'afterLogoutCleanUp', ->
diff --git a/packages/rocketchat-ui/lib/chatMessages.coffee b/packages/rocketchat-ui/lib/chatMessages.coffee
index 4847e6c102d4100d0c1ff329ab14ae97659915bc..c61851ff0c2b68433689b86241274aba884a766c 100644
--- a/packages/rocketchat-ui/lib/chatMessages.coffee
+++ b/packages/rocketchat-ui/lib/chatMessages.coffee
@@ -169,6 +169,7 @@ class @ChatMessages
 
 				KonchatNotification.removeRoomNotification(rid)
 				input.value = ''
+				input.updateAutogrow?()
 				this.hasValue.set false
 				this.stopTyping(rid)
 
@@ -379,7 +380,8 @@ class @ChatMessages
 			RoomHistoryManager.clear rid
 
 	valueChanged: (rid, event) ->
-		this.determineInputDirection()
+		if this.input.value.length is 1
+			this.determineInputDirection()
 
 	determineInputDirection: () ->
 		this.input.dir = if this.isMessageRtl(this.input.value) then 'rtl' else 'ltr'
diff --git a/packages/rocketchat-ui/lib/cordova/user-state.coffee b/packages/rocketchat-ui/lib/cordova/user-state.coffee
deleted file mode 100644
index f7b867a139d3b40dbc064b1abb1b2e4bd7bfb5f5..0000000000000000000000000000000000000000
--- a/packages/rocketchat-ui/lib/cordova/user-state.coffee
+++ /dev/null
@@ -1,8 +0,0 @@
-if Meteor.isCordova
-	document.addEventListener 'pause', ->
-		UserPresence.setAway()
-		readMessage.disable()
-
-	document.addEventListener 'resume', ->
-		UserPresence.setOnline()
-		readMessage.enable()
\ No newline at end of file
diff --git a/packages/rocketchat-ui/lib/cordova/user-state.js b/packages/rocketchat-ui/lib/cordova/user-state.js
new file mode 100644
index 0000000000000000000000000000000000000000..40f4deb58b65c3b3fa827606272376c577b5d042
--- /dev/null
+++ b/packages/rocketchat-ui/lib/cordova/user-state.js
@@ -0,0 +1,25 @@
+/* globals UserPresence, readMessage */
+
+var timer = undefined;
+if (Meteor.isCordova) {
+	document.addEventListener('pause', () => {
+		UserPresence.setAway();
+		readMessage.disable();
+
+		//Only disconnect after one minute of being in the background
+		timer = setTimeout(() => {
+			Meteor.disconnect();
+			timer = undefined;
+		}, 60000);
+	}, true);
+
+	document.addEventListener('resume', () => {
+		if (!_.isUndefined(timer)) {
+			clearTimeout(timer);
+		}
+
+		Meteor.reconnect();
+		UserPresence.setOnline();
+		readMessage.enable();
+	}, true);
+}
diff --git a/packages/rocketchat-ui/package.js b/packages/rocketchat-ui/package.js
index b9d2abe4e47a61e074a775fa5a4d66902e665e86..d0d60681384110b80076d370f43a27381376cf12 100644
--- a/packages/rocketchat-ui/package.js
+++ b/packages/rocketchat-ui/package.js
@@ -63,7 +63,7 @@ Package.onUse(function(api) {
 	api.addFiles('lib/cordova/keyboard-fix.coffee', 'client');
 	api.addFiles('lib/cordova/push.coffee', 'client');
 	api.addFiles('lib/cordova/urls.coffee', 'client');
-	api.addFiles('lib/cordova/user-state.coffee', 'client');
+	api.addFiles('lib/cordova/user-state.js', 'client');
 
 	// LIB RECORDERJS
 	api.addFiles('lib/recorderjs/audioRecorder.coffee', 'client');
diff --git a/packages/rocketchat-ui/views/app/room.coffee b/packages/rocketchat-ui/views/app/room.coffee
index af2f6b6f8f52d22451f7f580d72a0bd4af578230..066e449c444cd7c1dfef3a04483e870f8f731e83 100644
--- a/packages/rocketchat-ui/views/app/room.coffee
+++ b/packages/rocketchat-ui/views/app/room.coffee
@@ -127,6 +127,16 @@ Template.room.helpers
 	userCanDrop: ->
 		return userCanDrop @_id
 
+	canPreview: ->
+		room = Session.get('roomData' + this._id)
+		if room.t isnt 'c'
+			return true
+
+		if RocketChat.authz.hasAllPermission('preview-c-room')
+			return true
+
+		return RocketChat.models.Subscriptions.findOne({rid: this._id})?
+
 isSocialSharingOpen = false
 touchMoved = false
 
diff --git a/packages/rocketchat-ui/views/app/room.html b/packages/rocketchat-ui/views/app/room.html
index 424fa4af70169bfc38ffffabd4dbab6ef7a234df..22e571663e795e3b9c23470b3fa29ad9fb7975f1 100644
--- a/packages/rocketchat-ui/views/app/room.html
+++ b/packages/rocketchat-ui/views/app/room.html
@@ -69,20 +69,29 @@
 				<div class="jump-recent {{#unless hasMoreNext}}not{{/unless}}">
 					<button>{{_ "Jump_to_recent_messages"}} <i class="icon-level-down"></i></button>
 				</div>
+				{{#unless canPreview}}
+					<div class="content room-not-found">
+						<div>
+							{{_ "You_must_join_to_view_messages_in_this_channel"}}
+						</div>
+					</div>
+				{{/unless}}
 				<div class="wrapper {{#if hasMoreNext}}has-more-next{{/if}} {{hideUsername}} {{hideAvatar}}">
 					<ul aria-live="polite">
-						{{#if hasMore}}
-							<li class="load-more">
-								{{#if isLoading}}
-									<div class="load-more-loading">{{_ "Loading_more_from_history"}}...</div>
-								{{else}}
-									<button>{{_ "Has_more"}}...</button>
-								{{/if}}
-							</li>
-						{{else}}
-							<li class="start">
-								{{_ "Start_of_conversation"}}
-							</li>
+						{{#if canPreview}}
+							{{#if hasMore}}
+								<li class="load-more">
+									{{#if isLoading}}
+										<div class="load-more-loading">{{_ "Loading_more_from_history"}}...</div>
+									{{else}}
+										<button>{{_ "Has_more"}}...</button>
+									{{/if}}
+								</li>
+							{{else}}
+								<li class="start">
+									{{_ "Start_of_conversation"}}
+								</li>
+							{{/if}}
 						{{/if}}
 						{{#each messagesHistory}}
 							{{#nrr nrrargs 'message' .}}{{/nrr}}
diff --git a/packages/rocketchat-ui/views/app/videoCall/videoButtons.html b/packages/rocketchat-ui/views/app/videoCall/videoButtons.html
index d8f442d60ff704ea11351d6485286b55d59f520e..2e3e07653e24c149db341e39de6505a69fa65c3b 100644
--- a/packages/rocketchat-ui/views/app/videoCall/videoButtons.html
+++ b/packages/rocketchat-ui/views/app/videoCall/videoButtons.html
@@ -1,5 +1,5 @@
 <template name="videoButtons">
-	<div class="group-call-buttons">
+	<div class="buttons-group">
 		{{#if videoAvaliable}}
 			{{#unless videoActive}}
 				{{#if callInProgress}}
diff --git a/packages/rocketchat-ui/views/app/videoCall/videoCall.html b/packages/rocketchat-ui/views/app/videoCall/videoCall.html
index 4f43b38bccfe96bb871e7cff161f7cf3f8dbb3f8..7c818b314beed182c6aea4183a8c87f185cffa95 100644
--- a/packages/rocketchat-ui/views/app/videoCall/videoCall.html
+++ b/packages/rocketchat-ui/views/app/videoCall/videoCall.html
@@ -30,7 +30,7 @@
 						</div>
 					{{/each}}
 				</div>
-				<div class="group-call-buttons">
+				<div class="buttons-group">
 					{{#if videoActive}}
 						<button class="stop-call button red"><i class="icon-stop"></i>{{_ "Stop"}}</button>
 						{{#if audioEnabled}}
diff --git a/packages/rocketchat-videobridge/client/messageType.js b/packages/rocketchat-videobridge/lib/messageType.js
similarity index 100%
rename from packages/rocketchat-videobridge/client/messageType.js
rename to packages/rocketchat-videobridge/lib/messageType.js
diff --git a/packages/rocketchat-videobridge/package.js b/packages/rocketchat-videobridge/package.js
index bcd5835356dce355f62ca0983d11e2f0c5583359..47c0aaa4dbb2c322941e44ff1c143b8ac4ca7978 100644
--- a/packages/rocketchat-videobridge/package.js
+++ b/packages/rocketchat-videobridge/package.js
@@ -24,7 +24,9 @@ Package.onUse(function(api) {
 	api.addFiles('client/views/videoFlexTab.js', 'client');
 	api.addFiles('client/tabBar.js', 'client');
 	api.addFiles('client/actionLink.js', 'client');
-	api.addFiles('client/messageType.js', 'client');
+
+	//Need to register the messageType with both the server and client
+	api.addFiles('lib/messageType.js', ['client', 'server']);
 
 	api.addFiles('server/settings.js', 'server');
 	api.addFiles('server/models/Rooms.js', 'server');
diff --git a/server/lib/cordova.coffee b/server/lib/cordova.coffee
index 09401875ae9c4c83b928a10a2a41ff77a9198817..1a0aab0e35cd5b60b62c9b40ffc9c0173e75455b 100644
--- a/server/lib/cordova.coffee
+++ b/server/lib/cordova.coffee
@@ -46,6 +46,7 @@ Meteor.methods
 
 configurePush = ->
 	if RocketChat.settings.get 'Push_debug'
+		Push.debug = true
 		console.log 'Push: configuring...'
 
 	if RocketChat.settings.get('Push_enable') is true
diff --git a/server/methods/createChannel.coffee b/server/methods/createChannel.coffee
deleted file mode 100644
index 82ad5bfb2bbbadeeaa0133278177af243a3f77df..0000000000000000000000000000000000000000
--- a/server/methods/createChannel.coffee
+++ /dev/null
@@ -1,73 +0,0 @@
-Meteor.methods
-	createChannel: (name, members, readOnly) ->
-		if not Meteor.userId()
-			throw new Meteor.Error 'error-invalid-user', "Invalid user", { method: 'createChannel' }
-
-		try
-			nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$'
-		catch
-			nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$'
-
-		if not nameValidation.test name
-			throw new Meteor.Error 'error-invalid-name', "Invalid name", { method: 'createChannel' }
-
-		if RocketChat.authz.hasPermission(Meteor.userId(), 'create-c') isnt true
-			throw new Meteor.Error 'error-not-allowed', "Not allowed", { method: 'createChannel' }
-
-		now = new Date()
-		user = Meteor.user()
-
-		members.push user.username if user.username not in members
-
-		# avoid duplicate names
-		if RocketChat.models.Rooms.findOneByName name
-			if RocketChat.models.Rooms.findOneByName(name).archived
-				throw new Meteor.Error 'error-archived-duplicate-name', "There's an archived channel with name " + name, { method: 'createChannel', room_name: name }
-			else
-				throw new Meteor.Error 'error-duplicate-channel-name', "A channel with name '" + name + "' exists", { method: 'createChannel', room_name: name }
-
-		# name = s.slugify name
-
-		RocketChat.callbacks.run 'beforeCreateChannel', user,
-			t: 'c'
-			name: name
-			ts: now
-			ro: readOnly is true
-			sysMes: readOnly isnt true
-			usernames: members
-			u:
-				_id: user._id
-				username: user.username
-
-		# create new room
-		room = RocketChat.models.Rooms.createWithTypeNameUserAndUsernames 'c', name, user, members,
-			ts: now
-			ro: readOnly is true
-			sysMes: readOnly isnt true
-
-		for username in members
-			member = RocketChat.models.Users.findOneByUsername username
-			if not member?
-				continue
-
-			# make all room members muted by default, unless they have the post-read-only permission
-			if readOnly is true and RocketChat.authz.hasPermission(member._id, 'post-read-only') is false
-				RocketChat.models.Rooms.muteUsernameByRoomId room._id, username
-
-			extra = {}
-
-			if username is user.username
-				extra.ls = now
-				extra.open = true
-
-			RocketChat.models.Subscriptions.createWithRoomAndUser room, member, extra
-
-		# set creator as channel moderator.  permission limited to channel by scoping to rid
-		RocketChat.authz.addUserRoles(Meteor.userId(), ['owner'], room._id)
-
-		Meteor.defer ->
-			RocketChat.callbacks.run 'afterCreateChannel', user, room
-
-		return {
-			rid: room._id
-		}
diff --git a/server/methods/createPrivateGroup.coffee b/server/methods/createPrivateGroup.coffee
deleted file mode 100644
index 83339f90a32e48bd56705b7199ca81a060c902c5..0000000000000000000000000000000000000000
--- a/server/methods/createPrivateGroup.coffee
+++ /dev/null
@@ -1,57 +0,0 @@
-Meteor.methods
-	createPrivateGroup: (name, members, readOnly) ->
-		if not Meteor.userId()
-			throw new Meteor.Error 'error-invalid-user', "Invalid user", { method: 'createPrivateGroup' }
-
-		unless RocketChat.authz.hasPermission(Meteor.userId(), 'create-p')
-			throw new Meteor.Error 'error-not-allowed', "Not allowed", { method: 'createPrivateGroup' }
-
-		try
-			nameValidation = new RegExp '^' + RocketChat.settings.get('UTF8_Names_Validation') + '$'
-		catch
-			nameValidation = new RegExp '^[0-9a-zA-Z-_.]+$'
-
-		if not nameValidation.test name
-			throw new Meteor.Error 'error-invalid-name', "Invalid name", { method: 'createPrivateGroup' }
-
-		now = new Date()
-
-		me = Meteor.user()
-
-		members.push me.username
-
-		# name = s.slugify name
-
-		# avoid duplicate names
-		if RocketChat.models.Rooms.findOneByName name
-			if RocketChat.models.Rooms.findOneByName(name).archived
-				throw new Meteor.Error 'error-archived-duplicate-name', "There's an archived channel with name " + name, { method: 'createPrivateGroup', room_name: name }
-			else
-				throw new Meteor.Error 'error-duplicate-channel-name', "A channel with name '" + name + "' exists", { method: 'createPrivateGroup', room_name: name }
-
-		# create new room
-		room = RocketChat.models.Rooms.createWithTypeNameUserAndUsernames 'p', name, me, members,
-			ts: now
-			ro: readOnly is true
-			sysMes: readOnly isnt true
-
-		for username in members
-			member = RocketChat.models.Users.findOneByUsername(username, { fields: { username: 1 }})
-			if not member?
-				continue
-
-			extra = {}
-
-			if username is me.username
-				extra.ls = now
-			else
-				extra.alert = true
-
-			RocketChat.models.Subscriptions.createWithRoomAndUser room, member, extra
-
-		# set creator as group moderator.  permission limited to group by scoping to rid
-		RocketChat.authz.addUserRoles(Meteor.userId(), ['owner'], room._id)
-
-		return {
-			rid: room._id
-		}
diff --git a/server/methods/joinRoom.coffee b/server/methods/joinRoom.coffee
deleted file mode 100644
index fdda7e783068411e805079fd3baf60738c308270..0000000000000000000000000000000000000000
--- a/server/methods/joinRoom.coffee
+++ /dev/null
@@ -1,43 +0,0 @@
-Meteor.methods
-	joinRoom: (rid) ->
-		if not Meteor.userId()
-			throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'joinRoom' }
-
-		room = RocketChat.models.Rooms.findOneById rid
-
-		if not room?
-			throw new Meteor.Error 'error-invalid-room', 'Invalid room', { method: 'joinRoom' }
-
-		if room.t isnt 'c' or RocketChat.authz.hasPermission(Meteor.userId(), 'view-c-room') isnt true
-			throw new Meteor.Error 'error-not-allowed', 'Not allowed', { method: 'joinRoom' }
-
-		now = new Date()
-
-		# Check if user is already in room
-		subscription = RocketChat.models.Subscriptions.findOneByRoomIdAndUserId rid, Meteor.userId()
-		if subscription?
-			return
-
-		user = RocketChat.models.Users.findOneById Meteor.userId()
-
-		RocketChat.callbacks.run 'beforeJoinRoom', user, room
-
-		# Automatically mute users in read only rooms
-		
-		muted = room.ro and not RocketChat.authz.hasPermission(Meteor.userId(), 'post-read-only')
-
-		RocketChat.models.Rooms.addUsernameById rid, user.username, muted
-
-		RocketChat.models.Subscriptions.createWithRoomAndUser room, user,
-			ts: now
-			open: true
-			alert: true
-			unread: 1
-
-		RocketChat.models.Messages.createUserJoinWithRoomIdAndUser rid, user,
-			ts: now
-
-		Meteor.defer ->
-			RocketChat.callbacks.run 'afterJoinRoom', user, room
-
-		return true
diff --git a/server/methods/loadHistory.coffee b/server/methods/loadHistory.coffee
index 3b08ef8296434cacda943031d1fe644e822a25b5..bf92f320d395972405fffeb4b0c251d0abb0b050 100644
--- a/server/methods/loadHistory.coffee
+++ b/server/methods/loadHistory.coffee
@@ -4,7 +4,11 @@ Meteor.methods
 			throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'loadHistory' }
 
 		fromId = Meteor.userId()
-		unless Meteor.call 'canAccessRoom', rid, fromId
+		room = Meteor.call 'canAccessRoom', rid, fromId
+		unless room
+			return false
+
+		if room.t is 'c' and not RocketChat.authz.hasPermission(fromId, 'preview-c-room') and room.usernames.indexOf(room.username) is -1
 			return false
 
 		options =
diff --git a/server/methods/sendForgotPasswordEmail.coffee b/server/methods/sendForgotPasswordEmail.coffee
index 32adedb8fba99eefcc7bfdb294efbb354f24e9e2..549ded5c54f25053155d74414e58cd36bba4a8b2 100644
--- a/server/methods/sendForgotPasswordEmail.coffee
+++ b/server/methods/sendForgotPasswordEmail.coffee
@@ -1,8 +1,15 @@
 Meteor.methods
 	sendForgotPasswordEmail: (email) ->
-		user = RocketChat.models.Users.findOneByEmailAddress s.trim(email.toLowerCase())
+
+		email = s.trim(email)
+		user = RocketChat.models.Users.findOneByEmailAddress(email)
+		regex = new RegExp("^" + s.escapeRegExp(email) + "$", 'i')
+
+		email = _.find _.pluck(user.emails || [], 'address'), (userEmail) ->
+			return regex.test(userEmail)
 
 		if user?
-			Accounts.sendResetPasswordEmail(user._id, s.trim(email))
+			Accounts.sendResetPasswordEmail(user._id, email)
 			return true
+
 		return false
diff --git a/server/methods/setAvatarFromService.coffee b/server/methods/setAvatarFromService.coffee
index ad516e8107f949887e801b6a39ac0e53ac04f102..dedcddea7ad4b278285b33fb6e48c048ade464b9 100644
--- a/server/methods/setAvatarFromService.coffee
+++ b/server/methods/setAvatarFromService.coffee
@@ -8,53 +8,7 @@ Meteor.methods
 
 		user = Meteor.user()
 
-		if service is 'initials'
-			RocketChat.models.Users.setAvatarOrigin user._id, service
-			return
-
-		if service is 'url'
-			result = null
-
-			try
-				result = HTTP.get dataURI, npmRequestOptions: {encoding: 'binary'}
-			catch e
-				console.log "Error while handling the setting of the avatar from a url (#{dataURI}) for #{user.username}:", e
-				throw new Meteor.Error('error-avatar-url-handling', 'Error while handling avatar setting from a URL ('+ dataURI +') for ' + user.username, { method: 'setAvatarFromService', url: dataURI, username: user.username });
-
-			if result.statusCode isnt 200
-				console.log "Not a valid response, #{result.statusCode}, from the avatar url: #{dataURI}"
-				throw new Meteor.Error('error-avatar-invalid-url', 'Invalid avatar URL: ' +  dataURI, { method: 'setAvatarFromService', url: dataURI })
-
-			if not /image\/.+/.test result.headers['content-type']
-				console.log "Not a valid content-type from the provided url, #{result.headers['content-type']}, from the avatar url: #{dataURI}"
-				throw new Meteor.Error('error-avatar-invalid-url', 'Invalid avatar URL: ' +  dataURI, { method: 'setAvatarFromService', url: dataURI })
-
-			ars = RocketChatFile.bufferToStream new Buffer(result.content, 'binary')
-			RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{user.username}.jpg")
-			aws = RocketChatFileAvatarInstance.createWriteStream encodeURIComponent("#{user.username}.jpg"), result.headers['content-type']
-			aws.on 'end', Meteor.bindEnvironment ->
-				Meteor.setTimeout ->
-					console.log "Set #{user.username}'s avatar from the url: #{dataURI}"
-					RocketChat.models.Users.setAvatarOrigin user._id, service
-					RocketChat.Notifications.notifyAll 'updateAvatar', { username: user.username }
-				, 500
-
-			ars.pipe(aws)
-			return
-
-		{image, contentType} = RocketChatFile.dataURIParse dataURI
-
-		rs = RocketChatFile.bufferToStream new Buffer(image, 'base64')
-		RocketChatFileAvatarInstance.deleteFile encodeURIComponent("#{user.username}.jpg")
-		ws = RocketChatFileAvatarInstance.createWriteStream encodeURIComponent("#{user.username}.jpg"), contentType
-		ws.on 'end', Meteor.bindEnvironment ->
-			Meteor.setTimeout ->
-				RocketChat.models.Users.setAvatarOrigin user._id, service
-				RocketChat.Notifications.notifyAll 'updateAvatar', {username: user.username}
-			, 500
-
-		rs.pipe(ws)
-		return
+		return RocketChat.setUserAvatar(user, dataURI, contentType, service);
 
 DDPRateLimiter.addRule
 	type: 'method'
diff --git a/server/methods/updateUserUtcOffset.coffee b/server/methods/userSetUtcOffset.coffee
similarity index 74%
rename from server/methods/updateUserUtcOffset.coffee
rename to server/methods/userSetUtcOffset.coffee
index 9712ad9b5e7c9fbff0116a28c193b549e54ebefd..fb8a7dd662f9113595c4992b92a8516bd3c18586 100644
--- a/server/methods/updateUserUtcOffset.coffee
+++ b/server/methods/userSetUtcOffset.coffee
@@ -1,5 +1,5 @@
 Meteor.methods
-	updateUserUtcOffset: (utcOffset) ->
+	userSetUtcOffset: (utcOffset) ->
 		if not @userId?
 			return
 
@@ -9,6 +9,6 @@ Meteor.methods
 
 DDPRateLimiter.addRule
 	type: 'method'
-	name: 'updateUserUtcOffset'
+	name: 'userSetUtcOffset'
 	userId: -> return true
 , 1, 60000
diff --git a/server/startup/cron.coffee b/server/startup/cron.coffee
index c935e53f0c8ae1a713e902b2ec0e3071a002c8d5..a58810a4264b5f8ed844cd35a9df01fcd4cbb926 100644
--- a/server/startup/cron.coffee
+++ b/server/startup/cron.coffee
@@ -10,8 +10,8 @@ generateStatistics = ->
 	statistics = RocketChat.statistics.save()
 	statistics.host = Meteor.absoluteUrl()
 	if RocketChat.settings.get 'Statistics_reporting'
-		try	
-			HTTP.post 'https://rocket.chat/stats',
+		try
+			HTTP.post 'https://collector.rocket.chat/',
 				data: statistics
 		catch e
 			logger.warn('Failed to send usage report')
diff --git a/server/startup/migrations/v058.js b/server/startup/migrations/v058.js
new file mode 100644
index 0000000000000000000000000000000000000000..329900d3d5fef437db608a9e9e3cad3e4b86078b
--- /dev/null
+++ b/server/startup/migrations/v058.js
@@ -0,0 +1,11 @@
+RocketChat.Migrations.add({
+	version: 58,
+	up: function() {
+		RocketChat.models.Settings.update({ _id: 'Push_gateway', value: 'https://rocket.chat' }, {
+			$set: {
+				value: 'https://gateway.rocket.chat',
+				packageValue: 'https://gateway.rocket.chat'
+			}
+		});
+	}
+});
diff --git a/server/startup/roomPublishes.coffee b/server/startup/roomPublishes.coffee
index 4d73c6c11d4863b9c14313dbd1f02c304860a45e..c1c1c7397210f41154bc4c706a21bcd5c58c4fdb 100644
--- a/server/startup/roomPublishes.coffee
+++ b/server/startup/roomPublishes.coffee
@@ -14,6 +14,10 @@ Meteor.startup ->
 				jitsiTimeout: 1
 				description: 1
 				sysMes: 1
+				joinCodeRequired: 1
+
+		if RocketChat.authz.hasPermission(this.userId, 'view-join-code')
+			options.fields.joinCode = 1
 
 		if RocketChat.authz.hasPermission(this.userId, 'view-c-room')
 			return RocketChat.models.Rooms.findByTypeAndName 'c', identifier, options
@@ -21,6 +25,7 @@ Meteor.startup ->
 			roomId = RocketChat.models.Subscriptions.findByTypeNameAndUserId('c', identifier, this.userId).fetch()
 			if roomId.length > 0
 				return RocketChat.models.Rooms.findById(roomId[0]?.rid, options)
+
 		return this.ready()
 
 	RocketChat.roomTypes.setPublish 'p', (identifier) ->
diff --git a/server/stream/messages.coffee b/server/stream/messages.coffee
index ac005c558fae0ba66956cbeda35b0176a23713a6..45cd8cf9688d64aa98af2cbfbd1545fca01cf65b 100644
--- a/server/stream/messages.coffee
+++ b/server/stream/messages.coffee
@@ -4,7 +4,12 @@ msgStream.allowWrite('none')
 
 msgStream.allowRead (eventName) ->
 	try
-		return false if not Meteor.call 'canAccessRoom', eventName, this.userId
+		room = Meteor.call 'canAccessRoom', eventName, this.userId
+		if not room
+			return false
+
+		if room.t is 'c' and not RocketChat.authz.hasPermission(this.userId, 'preview-c-room') and room.usernames.indexOf(room.username) is -1
+			return false
 
 		return true
 	catch e
@@ -36,6 +41,6 @@ Meteor.startup ->
 		records = RocketChat.models.Messages.getChangedRecords type, args[0], fields
 
 		for record in records
-			if record._hidden isnt true
+			if record._hidden isnt true and not record.imported?
 				msgStream.emit '__my_messages__', record, {}
 				msgStream.emit record.rid, record
diff --git a/server/stream/streamBroadcast.coffee b/server/stream/streamBroadcast.coffee
index cfe6fca62238557ed181a00694b1c95c8e227161..82ca7b51d52c5af927528b99734897b74502d1a9 100644
--- a/server/stream/streamBroadcast.coffee
+++ b/server/stream/streamBroadcast.coffee
@@ -4,22 +4,40 @@ logger = new Logger 'StreamBroadcast',
 		auth: 'Auth'
 		stream: 'Stream'
 
-authorizeConnection = (instance) ->
+_authorizeConnection = (instance) ->
 	logger.auth.info "Authorizing with #{instance}"
+
 	connections[instance].call 'broadcastAuth', connections[instance].instanceRecord._id, InstanceStatus.id(), (err, ok) ->
+		if err?
+			return logger.auth.error "broadcastAuth error #{instance} #{connections[instance].instanceRecord._id} #{InstanceStatus.id()}", err
+
 		connections[instance].broadcastAuth = ok
 		logger.auth.info "broadcastAuth with #{instance}", ok
 
+authorizeConnection = (instance) ->
+	if not InstanceStatus.getCollection().findOne({_id: InstanceStatus.id()})?
+		return Meteor.setTimeout ->
+			authorizeConnection(instance)
+		, 500
+
+	_authorizeConnection(instance)
+
 @connections = {}
 @startStreamBroadcast = () ->
+	process.env.INSTANCE_IP ?= 'localhost'
+
 	logger.info 'startStreamBroadcast'
 
 	InstanceStatus.getCollection().find({'extraInformation.port': {$exists: true}}, {sort: {_createdAt: -1}}).observe
 		added: (record) ->
-			if record.extraInformation.port is process.env.PORT and (record.extraInformation.host is 'localhost' or record.extraInformation.host is process.env.INSTANCE_IP)
+			instance = "#{record.extraInformation.host}:#{record.extraInformation.port}"
+
+			if record.extraInformation.port is process.env.PORT and record.extraInformation.host is process.env.INSTANCE_IP
+				logger.auth.info "prevent self connect", instance
 				return
 
-			instance = record.extraInformation.host + ':' + record.extraInformation.port
+			if record.extraInformation.host is process.env.INSTANCE_IP
+				instance = "localhost:#{record.extraInformation.port}"
 
 			if connections[instance]?.instanceRecord?
 				if connections[instance].instanceRecord._createdAt < record._createdAt
@@ -31,19 +49,22 @@ authorizeConnection = (instance) ->
 			logger.connection.info 'connecting in', instance
 			connections[instance] = DDP.connect(instance, {_dontPrintErrors: true})
 			connections[instance].instanceRecord = record;
-			authorizeConnection(instance);
 			connections[instance].onReconnect = ->
-				authorizeConnection(instance);
+				authorizeConnection(instance)
 
 		removed: (record) ->
-			instance = record.extraInformation.host + ':' + record.extraInformation.port
+			instance = "#{record.extraInformation.host}:#{record.extraInformation.port}"
+
+			if record.extraInformation.host is process.env.INSTANCE_IP
+				instance = "localhost:#{record.extraInformation.port}"
+
 			if connections[instance]? and not InstanceStatus.getCollection().findOne({'extraInformation.host': record.extraInformation.host, 'extraInformation.port': record.extraInformation.port})?
 				logger.connection.info 'disconnecting from', instance
 				connections[instance].disconnect()
 				delete connections[instance]
 
 	broadcast = (streamName, eventName, args, userId) ->
-		fromInstance = (process.env.INSTANCE_IP or 'localhost') + ':' + process.env.PORT
+		fromInstance = process.env.INSTANCE_IP + ':' + process.env.PORT
 		for instance, connection of connections
 			do (instance, connection) ->
 				if connection.status().connected is true