-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
error
+
+ {{app.errorMessage}}
+
+
+
+
+
diff --git a/UI/WebServerResources/js/Common/sgRippleClick.directive.js b/UI/WebServerResources/js/Common/sgRippleClick.directive.js
new file mode 100644
index 000000000..eb1327880
--- /dev/null
+++ b/UI/WebServerResources/js/Common/sgRippleClick.directive.js
@@ -0,0 +1,108 @@
+/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+
+(function() {
+ 'use strict';
+
+ angular
+ .module('SOGo.Common')
+ .directive('sgRippleClick', sgRippleClick);
+
+ /*
+ * sgRippleClick - A ripple effect to cover the parent element.
+ * @memberof SOGo.Common
+ * @restrict attribute
+ *
+ * @example:
+
+
+ Send
+
+
+ */
+ sgRippleClick.$inject = ['$log', '$timeout'];
+ function sgRippleClick($log, $timeout) {
+
+ return {
+ restrict: 'A',
+ compile: compile
+ };
+
+ function compile(tElement, tAttrs) {
+
+ return function postLink(scope, element, attr) {
+ var ripple, content, container, containerId;
+
+ // Lookup container element
+ containerId = element.attr('sg-ripple-click');
+ container = element[0].parentNode;
+ while (container && container.id != containerId) {
+ container = container.parentNode;
+ }
+ if (!container) {
+ $log.error('No parent element found with id ' + containerId);
+ return undefined;
+ }
+
+ // Lookup sg-ripple-content element
+ content = container.querySelector('sg-ripple-content');
+ if (!content) {
+ $log.error('sg-ripple-content not found inside #' + containerId);
+ return undefined;
+ }
+
+ // Lookup sg-ripple element
+ ripple = container.querySelector('sg-ripple');
+ if (ripple) {
+ ripple = angular.element(ripple);
+ }
+ else {
+ // If ripple layer doesn't exit, create it with the primary background color
+ ripple = angular.element('
');
+ container.appendChild(ripple[0]);
+
+ // Hide ripple content on initialization
+ if (!content.classList.contains('ng-hide'))
+ content.classList.add('ng-hide');
+ }
+
+ // Register listener
+ element.on('click', listener);
+
+ function listener(event) {
+ if (element[0].hasAttribute('disabled')) {
+ return;
+ }
+
+ if (content.classList.contains('ng-hide')) {
+ // Show ripple
+ angular.element(container).css({ 'overflow': 'hidden' });
+ content.classList.remove('ng-hide');
+ angular.element(content).css({ top: container.scrollTop + 'px' });
+ ripple.css({
+ 'top': (event.pageY - container.offsetTop + container.scrollTop) + 'px',
+ 'left': (event.pageX - container.offsetLeft) + 'px',
+ 'width': '400vmin',
+ 'height': '400vmin'
+ });
+ }
+ else {
+ // Hide ripple layer
+ ripple.css({
+ 'top': (event.pageY - container.offsetTop + container.scrollTop) + 'px',
+ 'left': (event.pageX - container.offsetLeft) + 'px',
+ 'height': '0px',
+ 'width': '0px'
+ });
+ // Hide ripple content
+ content.classList.add('ng-hide');
+ // Restore overflow of container once the animation is completed
+ $timeout(function() {
+ angular.element(container).css({ 'overflow': '' });
+ }, 800);
+ }
+ }
+ };
+ }
+ }
+})();
diff --git a/UI/WebServerResources/js/Mailer/Message.service.js b/UI/WebServerResources/js/Mailer/Message.service.js
index fa0c1a5f2..1f3643132 100644
--- a/UI/WebServerResources/js/Mailer/Message.service.js
+++ b/UI/WebServerResources/js/Mailer/Message.service.js
@@ -636,18 +636,18 @@
Message.$log.debug('send = ' + JSON.stringify(data, undefined, 2));
- return Message.$$resource.post(this.$absolutePath({asDraft: true}), 'send', data).then(function(data) {
- if (data.status == 'success') {
+ return Message.$$resource.post(this.$absolutePath({asDraft: true}), 'send', data).then(function(response) {
+ if (response.status == 'success') {
if (angular.isDefined(_this.origin)) {
if (_this.origin.action.startsWith('reply'))
_this.origin.message.isanswered = true;
else if (_this.origin.action == 'forward')
_this.origin.message.isforwarded = true;
}
- return data;
+ return response;
}
else {
- return Message.$q.reject(data);
+ return Message.$q.reject(response.data);
}
});
};
diff --git a/UI/WebServerResources/js/Mailer/MessageEditorController.js b/UI/WebServerResources/js/Mailer/MessageEditorController.js
index 260553638..d28cf44d6 100644
--- a/UI/WebServerResources/js/Mailer/MessageEditorController.js
+++ b/UI/WebServerResources/js/Mailer/MessageEditorController.js
@@ -6,9 +6,9 @@
/**
* @ngInject
*/
- MessageEditorController.$inject = ['$window', '$stateParams', '$mdConstant', '$mdDialog', '$mdToast', 'FileUploader', 'stateAccount', 'stateMessage', 'encodeUriFilter', '$timeout', 'Dialog', 'AddressBook', 'Card', 'Preferences'];
- function MessageEditorController($window, $stateParams, $mdConstant, $mdDialog, $mdToast, FileUploader, stateAccount, stateMessage, encodeUriFilter, $timeout, Dialog, AddressBook, Card, Preferences) {
- var vm = this, semicolon = 186;
+ MessageEditorController.$inject = ['$scope', '$window', '$stateParams', '$mdConstant', '$mdDialog', '$mdToast', 'FileUploader', 'stateAccount', 'stateMessage', 'encodeUriFilter', '$timeout', 'Dialog', 'AddressBook', 'Card', 'Preferences'];
+ function MessageEditorController($scope, $window, $stateParams, $mdConstant, $mdDialog, $mdToast, FileUploader, stateAccount, stateMessage, encodeUriFilter, $timeout, Dialog, AddressBook, Card, Preferences) {
+ var vm = this;
vm.addRecipient = addRecipient;
vm.autocomplete = {to: {}, cc: {}, bcc: {}};
@@ -19,10 +19,16 @@
vm.cancel = cancel;
vm.save = save;
vm.send = send;
+ vm.sendState = false;
vm.removeAttachment = removeAttachment;
vm.contactFilter = contactFilter;
vm.identities = _.map(stateAccount.identities, 'full');
- vm.recipientSeparatorKeys = [$mdConstant.KEY_CODE.ENTER, $mdConstant.KEY_CODE.TAB, $mdConstant.KEY_CODE.COMMA, semicolon];
+ vm.recipientSeparatorKeys = [
+ $mdConstant.KEY_CODE.ENTER,
+ $mdConstant.KEY_CODE.TAB,
+ $mdConstant.KEY_CODE.COMMA,
+ $mdConstant.KEY_CODE.SEMICOLON
+ ];
vm.uploader = new FileUploader({
url: stateMessage.$absolutePath({asDraft: true}) + '/save',
autoUpload: true,
@@ -54,6 +60,9 @@
}
});
+ // Destroy file uploader when the controller is being deactivated
+ $scope.$on('$destroy', function() { vm.uploader.destroy(); });
+
if ($stateParams.actionName == 'reply') {
stateMessage.$reply().then(function(msgObject) {
vm.message = msgObject;
@@ -168,10 +177,13 @@
function send() {
var ctrls = $parentControllers();
+
+ vm.sendState = 'sending';
if (vm.autosave)
$timeout.cancel(vm.autosave);
vm.message.$send().then(function(data) {
+ vm.sendState = 'sent';
if (ctrls.draftMailboxCtrl) {
// We're sending a draft from a popup window and the draft mailbox is opened.
// Reload draft mailbox
@@ -192,7 +204,12 @@
.content(l('Your email has been sent'))
.position('top right')
.hideDelay(3000));
- $mdDialog.hide();
+
+ // Let the user see the succesfull message before closing the dialog
+ $timeout($mdDialog.hide, 1000);
+ }, function(response) {
+ vm.sendState = 'error';
+ vm.errorMessage = response.data? response.data.message : response.statusText;
});
}
diff --git a/UI/WebServerResources/js/Main/Main.app.js b/UI/WebServerResources/js/Main/Main.app.js
index 578daa4d0..239631e16 100644
--- a/UI/WebServerResources/js/Main/Main.app.js
+++ b/UI/WebServerResources/js/Main/Main.app.js
@@ -15,20 +15,29 @@
vm.creds = { username: cookieUsername, password: null };
vm.login = login;
+ vm.loginState = false;
vm.showAbout = showAbout;
+ // Show login once everything is initialized
vm.showLogin = false;
$timeout(function() { vm.showLogin = true; }, 100);
function login() {
+ vm.loginState = 'authenticating';
Authentication.login(vm.creds)
.then(function(url) {
- if (window.location.href === url)
- window.location.reload(true);
- else
- window.location.href = url;
+ vm.loginState = 'logged';
+
+ // Let the user see the succesfull message before reloading the page
+ $timeout(function() {
+ if (window.location.href === url)
+ window.location.reload(true);
+ else
+ window.location.href = url;
+ }, 1000);
}, function(msg) {
- Dialog.alert(l('Authentication Failed'), msg.error);
+ vm.loginState = 'error';
+ vm.errorMessage = msg.error;
});
return false;
}
diff --git a/UI/WebServerResources/scss/components/ripple/ripple.scss b/UI/WebServerResources/scss/components/ripple/ripple.scss
new file mode 100644
index 000000000..2e47c956d
--- /dev/null
+++ b/UI/WebServerResources/scss/components/ripple/ripple.scss
@@ -0,0 +1,21 @@
+/// ripple.scss -*- Mode: scss; indent-tabs-mode: nil; basic-offset: 2 -*-
+
+sg-ripple {
+ border-radius: 100%;
+ height: 0px;
+ width: 0px;
+ position: absolute;
+ transition: width 800ms linear, height 800ms linear, background-color 400ms linear;
+ transform: translate(-50%, -50%);
+ opacity: 1;
+ z-index: $z-index-toolbar + 1;
+}
+
+sg-ripple-content {
+ bottom: 0;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ z-index: $z-index-toolbar + 2;
+}
\ No newline at end of file
diff --git a/UI/WebServerResources/scss/core/structure.scss b/UI/WebServerResources/scss/core/structure.scss
index cb7af4279..a1f8ec329 100644
--- a/UI/WebServerResources/scss/core/structure.scss
+++ b/UI/WebServerResources/scss/core/structure.scss
@@ -29,16 +29,6 @@ html * {
}
}
-.sg-logo {
- background-image: url("../img/sogo-full.svg");
- background-size: contain;
- background-repeat: no-repeat;
- height: 7 * $bl;
- min-width: 18 * $bl;
- //optical adjustement
- transform: translateY(-10%);
-}
-
.sg-category {
position: absolute;
top: 0;
diff --git a/UI/WebServerResources/scss/styles.scss b/UI/WebServerResources/scss/styles.scss
index 945eae8a7..957fe140a 100755
--- a/UI/WebServerResources/scss/styles.scss
+++ b/UI/WebServerResources/scss/styles.scss
@@ -66,6 +66,7 @@
@import 'components/whiteframe/whiteframe';
// Inverse components
+@import 'components/ripple/ripple';
@import 'components/timepicker/timepicker';
// @import '../angular-material/src/core/style/color-palette';
diff --git a/UI/WebServerResources/scss/views/LoginUI.scss b/UI/WebServerResources/scss/views/LoginUI.scss
index 9b42ed9f4..b29f42f06 100644
--- a/UI/WebServerResources/scss/views/LoginUI.scss
+++ b/UI/WebServerResources/scss/views/LoginUI.scss
@@ -1,46 +1,76 @@
/// LoginUI.scss -*- Mode: scss; indent-tabs-mode: nil; basic-offset: 2 -*-
+$sg-login-width: grid-step(5);
+
[ui-view="login"] {
md-content {
- padding: 0;
- @include to(sm) {
- [id=logo] {
- text-align: center;
+ // Keep content centered
+ margin: auto;
+
+ .sg-logo img {
+ width: grid-step(5);
+ }
+
+ /**
+ * On small screens, we dispose the logo and login form as column.
+ */
+ @include to(md) {
+ & {
+ // Fill the screen
+ min-height: 100%;
+ }
+ .sg-logo {
+ // Center content
+ margin: auto;
img {
+ margin: auto;
+ height: 100%;
max-width: 75%;
}
}
+ .sg-login {
+ // Let the form take all available space
+ flex-grow: 1;
+ margin: 0;
+ }
+ .sg-login-content {
+ margin: auto;
+ max-width: $sg-login-width;
+ }
}
- @include from(sm) {
+
+ /**
+ * On larger screen, we dispose the logo and login form as a row.
+ */
+ @include from(md) {
&.ng-hide {
- [id=logo] {
+ .sg-logo img {
opacity: 0;
- transform: translateX(50%);
+ transform: translateX($sg-login-width/2 + $baseline-grid + $baseline-grid*2);
}
.sg-login {
opacity: 0;
transform: translateX(100%);
}
}
- [id=logo], .sg-login {
+ .sg-logo img, .sg-login {
opacity: 1;
- //transform: translateX(0%);
}
- [id=logo] {
- transition: transform $swift-ease-out-duration $swift-ease-out-timing-function 600ms,
- opacity 400ms linear;
+ .sg-logo {
+ max-height: 100%;
+ max-width: 50%;
+ img {
+ transition: transform $swift-ease-out-duration $swift-ease-out-timing-function 600ms,
+ opacity 400ms linear;
+ }
}
.sg-login {
+ max-width: 50%;
transition: all $swift-ease-out-duration $swift-ease-out-timing-function 600ms;
}
- }
+ .sg-login-content {
+ width: $sg-login-width;
+ }
+ } // from(md)
}
}
-
-[ui-view="login"] > md-content > div {
- width: grid-step(5);
-}
-
-[id=logo] img {
- max-width: 100%;
-}