diff --git a/webclient/app-directive.js b/webclient/app-directive.js
index 75283598ab..c1ba0af3a9 100644
--- a/webclient/app-directive.js
+++ b/webclient/app-directive.js
@@ -40,4 +40,45 @@ angular.module('matrixWebClient')
}
}
};
-}]);
\ No newline at end of file
+}])
+.directive('asjson', function() {
+ return {
+ restrict: 'A',
+ require: 'ngModel',
+ link: function (scope, element, attrs, ngModelCtrl) {
+ function isValidJson(model) {
+ var flag = true;
+ try {
+ angular.fromJson(model);
+ } catch (err) {
+ flag = false;
+ }
+ return flag;
+ };
+
+ function string2JSON(text) {
+ try {
+ var j = angular.fromJson(text);
+ ngModelCtrl.$setValidity('json', true);
+ return j;
+ } catch (err) {
+ //returning undefined results in a parser error as of angular-1.3-rc.0, and will not go through $validators
+ //return undefined
+ ngModelCtrl.$setValidity('json', false);
+ return text;
+ }
+ };
+
+ function JSON2String(object) {
+ return angular.toJson(object, true);
+ };
+
+ //$validators is an object, where key is the error
+ //ngModelCtrl.$validators.json = isValidJson;
+
+ //array pipelines
+ ngModelCtrl.$parsers.push(string2JSON);
+ ngModelCtrl.$formatters.push(JSON2String);
+ }
+ }
+});
diff --git a/webclient/app.css b/webclient/app.css
index 20a13aad81..5ab8e2b8fd 100755
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -418,6 +418,37 @@ textarea, input {
margin-top: 15px;
}
+/*** Room Info Dialog ***/
+
+.room-info {
+ border-collapse: collapse;
+ width: 100%;
+}
+
+.room-info-event {
+ border-bottom: 1pt solid black;
+}
+
+.room-info-event-meta {
+ padding-top: 1em;
+ padding-bottom: 1em;
+}
+
+.room-info-event-content {
+ padding-top: 1em;
+ padding-bottom: 1em;
+}
+
+.monospace {
+ font-family: monospace;
+}
+
+.room-info-textarea-content {
+ height: auto;
+ width: 100%;
+ resize: vertical;
+}
+
/*** Participant list ***/
#usersTableWrapper {
diff --git a/webclient/app.js b/webclient/app.js
index 099e2170a0..8d9b662ee9 100644
--- a/webclient/app.js
+++ b/webclient/app.js
@@ -31,7 +31,8 @@ var matrixWebClient = angular.module('matrixWebClient', [
'eventStreamService',
'eventHandlerService',
'infinite-scroll',
- 'ui.bootstrap'
+ 'ui.bootstrap',
+ 'monospaced.elastic'
]);
matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index 3b1354cdef..6f251eec56 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -564,6 +564,13 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
handleRedaction(event, isLiveEvent);
break;
default:
+ // if it is a state event, then just add it in so it
+ // displays on the Room Info screen.
+ if (typeof(event.state_key) === "string") { // incls. 0-len strings
+ if (event.room_id) {
+ handleRoomDateEvent(event, isLiveEvent, false);
+ }
+ }
console.log("Unable to handle event type " + event.type);
console.log(JSON.stringify(event, undefined, 4));
break;
diff --git a/webclient/index.html b/webclient/index.html
index 35c8051298..d8b9c95353 100644
--- a/webclient/index.html
+++ b/webclient/index.html
@@ -20,6 +20,7 @@
+
diff --git a/webclient/js/elastic.js b/webclient/js/elastic.js
new file mode 100644
index 0000000000..d585d81109
--- /dev/null
+++ b/webclient/js/elastic.js
@@ -0,0 +1,216 @@
+/*
+ * angular-elastic v2.4.0
+ * (c) 2014 Monospaced http://monospaced.com
+ * License: MIT
+ */
+
+angular.module('monospaced.elastic', [])
+
+ .constant('msdElasticConfig', {
+ append: ''
+ })
+
+ .directive('msdElastic', [
+ '$timeout', '$window', 'msdElasticConfig',
+ function($timeout, $window, config) {
+ 'use strict';
+
+ return {
+ require: 'ngModel',
+ restrict: 'A, C',
+ link: function(scope, element, attrs, ngModel) {
+
+ // cache a reference to the DOM element
+ var ta = element[0],
+ $ta = element;
+
+ // ensure the element is a textarea, and browser is capable
+ if (ta.nodeName !== 'TEXTAREA' || !$window.getComputedStyle) {
+ return;
+ }
+
+ // set these properties before measuring dimensions
+ $ta.css({
+ 'overflow': 'hidden',
+ 'overflow-y': 'hidden',
+ 'word-wrap': 'break-word'
+ });
+
+ // force text reflow
+ var text = ta.value;
+ ta.value = '';
+ ta.value = text;
+
+ var append = attrs.msdElastic ? attrs.msdElastic.replace(/\\n/g, '\n') : config.append,
+ $win = angular.element($window),
+ mirrorInitStyle = 'position: absolute; top: -999px; right: auto; bottom: auto;' +
+ 'left: 0; overflow: hidden; -webkit-box-sizing: content-box;' +
+ '-moz-box-sizing: content-box; box-sizing: content-box;' +
+ 'min-height: 0 !important; height: 0 !important; padding: 0;' +
+ 'word-wrap: break-word; border: 0;',
+ $mirror = angular.element('').data('elastic', true),
+ mirror = $mirror[0],
+ taStyle = getComputedStyle(ta),
+ resize = taStyle.getPropertyValue('resize'),
+ borderBox = taStyle.getPropertyValue('box-sizing') === 'border-box' ||
+ taStyle.getPropertyValue('-moz-box-sizing') === 'border-box' ||
+ taStyle.getPropertyValue('-webkit-box-sizing') === 'border-box',
+ boxOuter = !borderBox ? {width: 0, height: 0} : {
+ width: parseInt(taStyle.getPropertyValue('border-right-width'), 10) +
+ parseInt(taStyle.getPropertyValue('padding-right'), 10) +
+ parseInt(taStyle.getPropertyValue('padding-left'), 10) +
+ parseInt(taStyle.getPropertyValue('border-left-width'), 10),
+ height: parseInt(taStyle.getPropertyValue('border-top-width'), 10) +
+ parseInt(taStyle.getPropertyValue('padding-top'), 10) +
+ parseInt(taStyle.getPropertyValue('padding-bottom'), 10) +
+ parseInt(taStyle.getPropertyValue('border-bottom-width'), 10)
+ },
+ minHeightValue = parseInt(taStyle.getPropertyValue('min-height'), 10),
+ heightValue = parseInt(taStyle.getPropertyValue('height'), 10),
+ minHeight = Math.max(minHeightValue, heightValue) - boxOuter.height,
+ maxHeight = parseInt(taStyle.getPropertyValue('max-height'), 10),
+ mirrored,
+ active,
+ copyStyle = ['font-family',
+ 'font-size',
+ 'font-weight',
+ 'font-style',
+ 'letter-spacing',
+ 'line-height',
+ 'text-transform',
+ 'word-spacing',
+ 'text-indent'];
+
+ // exit if elastic already applied (or is the mirror element)
+ if ($ta.data('elastic')) {
+ return;
+ }
+
+ // Opera returns max-height of -1 if not set
+ maxHeight = maxHeight && maxHeight > 0 ? maxHeight : 9e4;
+
+ // append mirror to the DOM
+ if (mirror.parentNode !== document.body) {
+ angular.element(document.body).append(mirror);
+ }
+
+ // set resize and apply elastic
+ $ta.css({
+ 'resize': (resize === 'none' || resize === 'vertical') ? 'none' : 'horizontal'
+ }).data('elastic', true);
+
+ /*
+ * methods
+ */
+
+ function initMirror() {
+ var mirrorStyle = mirrorInitStyle;
+
+ mirrored = ta;
+ // copy the essential styles from the textarea to the mirror
+ taStyle = getComputedStyle(ta);
+ angular.forEach(copyStyle, function(val) {
+ mirrorStyle += val + ':' + taStyle.getPropertyValue(val) + ';';
+ });
+ mirror.setAttribute('style', mirrorStyle);
+ }
+
+ function adjust() {
+ var taHeight,
+ taComputedStyleWidth,
+ mirrorHeight,
+ width,
+ overflow;
+
+ if (mirrored !== ta) {
+ initMirror();
+ }
+
+ // active flag prevents actions in function from calling adjust again
+ if (!active) {
+ active = true;
+
+ mirror.value = ta.value + append; // optional whitespace to improve animation
+ mirror.style.overflowY = ta.style.overflowY;
+
+ taHeight = ta.style.height === '' ? 'auto' : parseInt(ta.style.height, 10);
+
+ taComputedStyleWidth = getComputedStyle(ta).getPropertyValue('width');
+
+ // ensure getComputedStyle has returned a readable 'used value' pixel width
+ if (taComputedStyleWidth.substr(taComputedStyleWidth.length - 2, 2) === 'px') {
+ // update mirror width in case the textarea width has changed
+ width = parseInt(taComputedStyleWidth, 10) - boxOuter.width;
+ mirror.style.width = width + 'px';
+ }
+
+ mirrorHeight = mirror.scrollHeight;
+
+ if (mirrorHeight > maxHeight) {
+ mirrorHeight = maxHeight;
+ overflow = 'scroll';
+ } else if (mirrorHeight < minHeight) {
+ mirrorHeight = minHeight;
+ }
+ mirrorHeight += boxOuter.height;
+
+ ta.style.overflowY = overflow || 'hidden';
+
+ if (taHeight !== mirrorHeight) {
+ ta.style.height = mirrorHeight + 'px';
+ scope.$emit('elastic:resize', $ta);
+ }
+
+ // small delay to prevent an infinite loop
+ $timeout(function() {
+ active = false;
+ }, 1);
+
+ }
+ }
+
+ function forceAdjust() {
+ active = false;
+ adjust();
+ }
+
+ /*
+ * initialise
+ */
+
+ // listen
+ if ('onpropertychange' in ta && 'oninput' in ta) {
+ // IE9
+ ta['oninput'] = ta.onkeyup = adjust;
+ } else {
+ ta['oninput'] = adjust;
+ }
+
+ $win.bind('resize', forceAdjust);
+
+ scope.$watch(function() {
+ return ngModel.$modelValue;
+ }, function(newValue) {
+ forceAdjust();
+ });
+
+ scope.$on('elastic:adjust', function() {
+ initMirror();
+ forceAdjust();
+ });
+
+ $timeout(adjust);
+
+ /*
+ * destroy
+ */
+
+ scope.$on('$destroy', function() {
+ $mirror.remove();
+ $win.unbind('resize', forceAdjust);
+ });
+ }
+ };
+ }
+ ]);
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index 37f51c4e91..59274baccb 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -1018,6 +1018,20 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
};
$scope.openRoomInfo = function() {
+ $scope.roomInfo = {};
+ $scope.roomInfo.newEvent = {
+ content: {},
+ type: "",
+ state_key: ""
+ };
+
+ var stateFilter = $filter("stateEventsFilter");
+ var stateEvents = stateFilter($scope.events.rooms[$scope.room_id]);
+ // The modal dialog will 2-way bind this field, so we MUST make a deep
+ // copy of the state events else we will be *actually adjusing our view
+ // of the world* when fiddling with the JSON!! Apparently parse/stringify
+ // is faster than jQuery's extend when doing deep copies.
+ $scope.roomInfo.stateEvents = JSON.parse(JSON.stringify(stateEvents));
var modalInstance = $modal.open({
templateUrl: 'roomInfoTemplate.html',
controller: 'RoomInfoController',
@@ -1036,12 +1050,21 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
$modalInstance.close("redact");
};
})
-.controller('RoomInfoController', function($scope, $modalInstance, $filter) {
+.controller('RoomInfoController', function($scope, $modalInstance, $filter, matrixService) {
console.log("Displaying room info.");
- $scope.submitState = function(eventType, content) {
- console.log("Submitting " + eventType + " with " + content);
- }
+ $scope.submit = function(event) {
+ if (event.content) {
+ console.log("submit >>> " + JSON.stringify(event.content));
+ matrixService.sendStateEvent($scope.room_id, event.type,
+ event.content, event.state_key).then(function(response) {
+ $modalInstance.dismiss();
+ }, function(err) {
+ $scope.feedback = err.data.error;
+ }
+ );
+ }
+ };
$scope.dismiss = $modalInstance.dismiss;
diff --git a/webclient/room/room.html b/webclient/room/room.html
index 3458e97039..fac7433a4b 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -15,23 +15,36 @@