diff options
37 files changed, 2666 insertions, 29 deletions
diff --git a/core/Controller/ContactsMenuController.php b/core/Controller/ContactsMenuController.php new file mode 100644 index 00000000000..edd2f2c8be8 --- /dev/null +++ b/core/Controller/ContactsMenuController.php @@ -0,0 +1,61 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Core\Controller; + +use OC\Contacts\ContactsMenu\Manager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; + +class ContactsMenuController extends Controller { + + /** @var Manager */ + private $manager; + + /** @var string */ + private $userId; + + /** + * @param IRequest $request + * @param string $UserId + * @param Manager $manager + */ + public function __construct(IRequest $request, $UserId, Manager $manager) { + parent::__construct('core', $request); + $this->userId = $UserId; + $this->manager = $manager; + } + + /** + * @NoAdminRequired + * + * @param string|null filter + * @return JSONResponse + */ + public function index($filter = null) { + return $this->manager->getEntries($this->userId, $filter); + } + +} diff --git a/core/css/header.scss b/core/css/header.scss index 619852faf60..50d270a6ff9 100644 --- a/core/css/header.scss +++ b/core/css/header.scss @@ -20,10 +20,21 @@ -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; +} - /* Dropdown menu arrow */ - &.menu:after, - .menu:after { +/* Header menu */ +.menu { + position: absolute; + top: 45px; + background-color: #fff; + box-shadow: 0 1px 10px rgba(150, 150, 150, 0.75); + border-radius: 0 0 3px 3px; + display: none; + box-sizing: border-box; + z-index: 2000; + + /* Dropdown arrow */ + &:after { border: 10px solid transparent; border-bottom-color: $color-main-background; bottom: 100%; @@ -199,19 +210,12 @@ nav { #navigation { position: relative; - top: 45px; left: -100%; width: 160px; - margin-top: 0; background-color: $color-main-background; box-shadow: 0 1px 10px $color-box-shadow; - border-radius: 3px; - border-top-left-radius: 0; - border-top-right-radius: 0; - display: none; - box-sizing: border-box; - z-index: 2000; &:after { + /* position of dropdown arrow */ left: 47%; bottom: 100%; border: solid transparent; @@ -407,17 +411,9 @@ nav { } #expanddiv { - position: absolute; right: 13px; - top: 45px; - z-index: 2000; - display: none; background: $color-main-background; box-shadow: 0 1px 10px $color-box-shadow; - border-radius: 3px; - border-top-left-radius: 0; - border-top-right-radius: 0; - box-sizing: border-box; &:after { /* position of dropdown arrow */ right: 13px; diff --git a/core/css/icons.scss b/core/css/icons.scss index 1ca29f22600..f9b73f51923 100644 --- a/core/css/icons.scss +++ b/core/css/icons.scss @@ -438,6 +438,10 @@ img, object, video, button, textarea, input, select { background-image: url('../img/places/calendar-dark.svg?v=1'); } +.icon-contacts { + background-image: url('../img/places/contacts.svg?v=1'); +} + .icon-contacts-dark { background-image: url('../img/places/contacts-dark.svg?v=1'); } diff --git a/core/css/styles.scss b/core/css/styles.scss index a6970336c12..ed11fd52158 100644 --- a/core/css/styles.scss +++ b/core/css/styles.scss @@ -1057,6 +1057,121 @@ span.ui-icon { margin: 3px 7px 30px 0; } +/* ---- CONTACTS MENU ---- */ + +#contactsmenu { + .menutoggle { + background-size: 16px 16px; + padding: 14px; + cursor: pointer; + opacity: .7; + } +} + +#contactsmenu > .menu { + /* show ~4.5 entries */ + height: 278px; + width: 350px; + right: 13px; + + &::after { + right: 61px; + } + + .emptycontent { + margin-top: 5vh !important; + margin-bottom: 2vh; + .icon-loading { + position: inherit; + } + .icon-search { + display: inline-block; + } + } + + .content { + max-height: calc(100% - 50px); + overflow-y: auto; + + .footer { + text-align: center; + + a { + display: block; + width: 100%; + padding: 12px 0; + opacity: .5; + } + } + } + + .contact { + display: flex; + position: relative; + align-items: center; + padding: 3px 3px 3px 10px; + border-bottom: 1px solid #eeeeee; + + :last-of-type { + border-bottom: none; + } + + .avatar { + height: 32px; + width: 32px; + display: inline-block; + } + + .body { + flex-grow: 1; + padding-left: 8px; + + div { + position: relative; + width: 100%; + } + + .full-name, .last-message { + /* TODO: don't use fixed width */ + max-width: 204px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + .last-message { + opacity: .5; + } + } + + .top-action, .second-action, .other-actions { + width: 16px; + height: 16px; + padding: 14px; + opacity: .5; + cursor: pointer; + + :hover { + opacity: 1; + } + } + + /* actions menu */ + .menu { + top: 47px; + margin-right: 13px; + } + .popovermenu::after { + right: 2px; + } + } +} + + +#contactsmenu-search { + width: calc(100% - 16px); + margin: 8px; +} + /* ---- TOOLTIPS ---- */ .extra-data { diff --git a/core/img/places/contacts.svg b/core/img/places/contacts.svg new file mode 100644 index 00000000000..fb6a60c0844 --- /dev/null +++ b/core/img/places/contacts.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" viewbox="0 0 32 32"><path style="block-progression:tb;text-transform:none;text-indent:0" d="M9.24 6.67c-1.955 0-3.613 1.43-3.613 3.275.014.583.066 1.302.414 2.823v.037l.038.038c.112.32.275.503.49.753.215.25.47.544.715.79l.075.076c.048.21.107.436.15.64.117.54.105.922.076 1.053-.84.295-1.885.647-2.823 1.054-.526.228-1.002.433-1.392.677-.39.244-.777.43-.903.978a.473.473 0 0 0 0 .076c-.123 1.13-.31 2.793-.452 3.915a.618.618 0 0 0 .3.603c1.704.92 4.32 1.29 6.927 1.28 2.607-.01 5.202-.403 6.85-1.28a.618.618 0 0 0 .3-.603c-.044-.35-.1-1.14-.15-1.92-.05-.778-.09-1.543-.15-1.994a.607.607 0 0 0-.15-.3c-.524-.626-1.306-1.008-2.22-1.393-.836-.352-1.815-.717-2.786-1.13-.055-.12-.11-.473 0-1.016.03-.144.074-.3.113-.45l.263-.3c.216-.248.447-.506.64-.754.192-.25.35-.462.452-.753l.037-.038c.393-1.588.393-2.25.413-2.823v-.037c0-1.845-1.658-3.275-3.613-3.275zm10.336-3.005c-2.85 0-5.268 2.084-5.268 4.774.02.85.096 1.898.604 4.115v.055l.055.055c.162.466.4.733.713 1.097s.687.793 1.043 1.153c.04.042.068.068.11.11.07.306.155.636.22.932.168.788.15 1.346.11 1.537-1.226.43-2.75.942-4.117 1.536-.768.334-1.462.632-2.03.988-.57.356-1.134.625-1.317 1.427a.67.67 0 0 0 0 .11c-.18 1.648-.452 4.07-.66 5.707a.9.9 0 0 0 .44.878c2.48 1.34 6.295 1.88 10.096 1.865s7.584-.586 9.987-1.865a.9.9 0 0 0 .44-.878c-.067-.512-.148-1.665-.22-2.8-.072-1.133-.134-2.25-.22-2.907a.884.884 0 0 0-.22-.44c-.763-.91-1.903-1.468-3.237-2.03-1.217-.513-2.645-1.045-4.06-1.646-.08-.177-.16-.69 0-1.483.042-.212.108-.44.164-.658.133-.15.237-.272.384-.44.315-.36.652-.735.933-1.098.28-.362.51-.673.66-1.097l.053-.055c.574-2.315.574-3.28.604-4.116V8.44c0-2.69-2.418-4.775-5.268-4.775z" fill="#fff"/></svg> diff --git a/core/js/contactsmenu.js b/core/js/contactsmenu.js new file mode 100644 index 00000000000..2991a0e84d1 --- /dev/null +++ b/core/js/contactsmenu.js @@ -0,0 +1,515 @@ +/* global OC.Backbone, Handlebars, Promise, _ */ + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +(function(OC, $, _, Handlebars) { + 'use strict'; + + var MENU_TEMPLATE = '' + + '<input id="contactsmenu-search" type="search" placeholder="Search contacts …" value="{{searchTerm}}">' + + '<div class="content">' + + '</div>'; + var CONTACTS_LIST_TEMPLATE = '' + + '{{#unless contacts.length}}' + + '<div class="emptycontent">' + + ' <a class="icon-search"></a>' + + ' <h2>' + t('core', 'No contacts found') + '</h2>' + + '</div>' + + '{{/unless}}' + + '<div id="contactsmenu-contacts"></div>' + + '{{#if contactsAppEnabled}}<div class="footer"><a href="{{contactsAppURL}}">' + t('core', 'Show all contacts …') + '</a></div>{{/if}}'; + var LOADING_TEMPLATE = '' + + '<div class="emptycontent">' + + ' <a class="icon-loading"></a>' + + ' <h2>{{loadingText}}</h2>' + + '</div>'; + var ERROR_TEMPLATE = '' + + '<div class="emptycontent">' + + ' <h2>' + t('core', 'Could not load your contacts.') + '</h2>' + + '</div>'; + var CONTACT_TEMPLATE = '' + + '{{#if contact.avatar}}' + + '<img src="{{contact.avatar}}" class="avatar">' + + '{{else}}' + + '<div class="avatar"></div>' + + '{{/if}}' + + '<div class="body">' + + ' <div class="full-name">{{contact.fullName}}</div>' + + ' <div class="last-message">{{contact.lastMessage}}</div>' + + '</div>' + + '{{#if contact.topAction}}' + + '<a class="top-action {{contact.topAction.icon}}" href="{{contact.topAction.hyperlink}}"></a>' + + '{{/if}}' + + '{{#if contact.hasManyActions}}' + + ' <span class="other-actions icon-more"></span>' + + ' <div class="menu popovermenu">' + + ' <ul>' + + ' {{#each contact.actions}}' + + ' <li>' + + ' <a href="{{hyperlink}}">' + + ' <span class="{{icon}}"></span>' + + ' <span>{{title}}</span>' + + ' </a>' + + ' </li>' + + ' {{/each}}' + + ' </ul>' + + ' </div>' + + '{{/if}}' + + '{{#if contact.hasTwoActions}}' + + '<a class="second-action {{contact.secondAction.icon}}" href="{{contact.secondAction.hyperlink}}"></a>' + + '{{/if}}'; + + /** + * @class Contact + */ + var Contact = OC.Backbone.Model.extend({ + defaults: { + fullName: '', + lastMessage: '', + actions: [], + hasOneAction: false, + hasTwoActions: false, + hasManyActions: false + }, + + /** + * @returns {undefined} + */ + initialize: function() { + // Add needed property for easier template rendering + if (this.get('actions').length === 0) { + this.set('hasOneAction', true); + } else if (this.get('actions').length === 1) { + this.set('hasTwoActions', true); + this.set('secondAction', this.get('actions')[0]); + } else { + this.set('hasManyActions', true); + } + } + }); + + /** + * @class ContactCollection + */ + var ContactCollection = OC.Backbone.Collection.extend({ + model: Contact + }); + + /** + * @class ContactsListView + */ + var ContactsListView = OC.Backbone.View.extend({ + + /** @type {ContactsCollection} */ + _collection: undefined, + + /** @type {array} */ + _subViews: [], + + /** + * @param {object} options + * @returns {undefined} + */ + initialize: function(options) { + this._collection = options.collection; + }, + + /** + * @returns {self} + */ + render: function() { + var self = this; + self.$el.html(''); + self._subViews = []; + + self._collection.forEach(function(contact) { + var item = new ContactsListItemView({ + model: contact + }); + item.render(); + self.$el.append(item.$el); + item.on('toggle:actionmenu', self._onChildActionMenuToggle, self); + self._subViews.push(item); + }); + + return self; + }, + + /** + * Event callback to propagate opening (another) entry's action menu + * + * @param {type} $src + * @returns {undefined} + */ + _onChildActionMenuToggle: function($src) { + this._subViews.forEach(function(view) { + view.trigger('parent:toggle:actionmenu', $src); + }); + } + }); + + /** + * @class CotnactsListItemView + */ + var ContactsListItemView = OC.Backbone.View.extend({ + + /** @type {string} */ + className: 'contact', + + /** @type {undefined|function} */ + _template: undefined, + + /** @type {Contact} */ + _model: undefined, + + /** @type {boolean} */ + _actionMenuShown: false, + + events: { + 'click .icon-more': '_onToggleActionsMenu' + }, + + /** + * @param {object} data + * @returns {undefined} + */ + template: function(data) { + if (!this._template) { + this._template = Handlebars.compile(CONTACT_TEMPLATE); + } + return this._template(data); + }, + + /** + * @param {object} options + * @returns {undefined} + */ + initialize: function(options) { + this._model = options.model; + this.on('parent:toggle:actionmenu', this._onOtherActionMenuOpened, this); + }, + + /** + * @returns {self} + */ + render: function() { + this.$el.html(this.template({ + contact: this._model.toJSON() + })); + this.delegateEvents(); + + // Show placeholder iff no avatar is available (avatar is rendered as img, not div) + this.$('div.avatar').imageplaceholder(this._model.get('fullName')); + + return this; + }, + + /** + * Toggle the visibility of the action popover menu + * + * @private + * @returns {undefined} + */ + _onToggleActionsMenu: function() { + this._actionMenuShown = !this._actionMenuShown; + if (this._actionMenuShown) { + this.$('.menu').show(); + } else { + this.$('.menu').hide(); + } + this.trigger('toggle:actionmenu', this.$el); + }, + + /** + * @private + * @argument {jQuery} $src + * @returns {undefined} + */ + _onOtherActionMenuOpened: function($src) { + if (this.$el.is($src)) { + // Ignore + return; + } + this._actionMenuShown = false; + this.$('.menu').hide(); + } + }); + + /** + * @class ContactsMenuView + */ + var ContactsMenuView = OC.Backbone.View.extend({ + + /** @type {undefined|function} */ + _loadingTemplate: undefined, + + /** @type {undefined|function} */ + _errorTemplate: undefined, + + /** @type {undefined|function} */ + _contentTemplate: undefined, + + /** @type {undefined|function} */ + _contactsTemplate: undefined, + + /** @type {undefined|ContactCollection} */ + _contacts: undefined, + + events: { + 'input #contactsmenu-search': '_onSearch' + }, + + /** + * @returns {undefined} + */ + _onSearch: _.debounce(function() { + this.trigger('search', this.$('#contactsmenu-search').val()); + }, 700), + + /** + * @param {object} data + * @returns {string} + */ + loadingTemplate: function(data) { + if (!this._loadingTemplate) { + this._loadingTemplate = Handlebars.compile(LOADING_TEMPLATE); + } + return this._loadingTemplate(data); + }, + + /** + * @param {object} data + * @returns {string} + */ + errorTemplate: function(data) { + if (!this._errorTemplate) { + this._errorTemplate = Handlebars.compile(ERROR_TEMPLATE); + } + return this._errorTemplate(data); + }, + + /** + * @param {object} data + * @returns {string} + */ + contentTemplate: function(data) { + if (!this._contentTemplate) { + this._contentTemplate = Handlebars.compile(MENU_TEMPLATE); + } + return this._contentTemplate(data); + }, + + /** + * @param {object} data + * @returns {string} + */ + contactsTemplate: function(data) { + if (!this._contactsTemplate) { + this._contactsTemplate = Handlebars.compile(CONTACTS_LIST_TEMPLATE); + } + return this._contactsTemplate(data); + }, + + /** + * @param {object} options + * @returns {undefined} + */ + initialize: function(options) { + this.options = options; + }, + + /** + * @param {string} text + * @returns {undefined} + */ + showLoading: function(text) { + this.render(); + this._contacts = undefined; + this.$('.content').html(this.loadingTemplate({ + loadingText: text + })); + }, + + /** + * @returns {undefined} + */ + showError: function() { + this.render(); + this._contacts = undefined; + this.$('.content').html(this.errorTemplate()); + }, + + /** + * @param {object} viewData + * @param {string} searchTerm + * @returns {undefined} + */ + showContacts: function(viewData, searchTerm) { + this._contacts = viewData.contacts; + this.render({ + contacts: viewData.contacts + }); + + var list = new ContactsListView({ + collection: viewData.contacts + }); + list.render(); + this.$('.content').html(this.contactsTemplate({ + contacts: viewData.contacts, + searchTerm: searchTerm, + contactsAppEnabled: viewData.contactsAppEnabled, + contactsAppURL: OC.generateUrl('/apps/contacts') + })); + this.$('#contactsmenu-contacts').html(list.$el); + }, + + /** + * @param {object} data + * @returns {self} + */ + render: function(data) { + var searchVal = this.$('#contactsmenu-search').val(); + this.$el.html(this.contentTemplate(data)); + + // Focus search + this.$('#contactsmenu-search').val(searchVal); + this.$('#contactsmenu-search').focus(); + return this; + } + + }); + + /** + * @param {Object} options + * @param {jQuery} options.el + * @param {jQuery} options.trigger + * @class ContactsMenu + */ + var ContactsMenu = function(options) { + this.initialize(options); + }; + + ContactsMenu.prototype = { + /** @type {jQuery} */ + $el: undefined, + + /** @type {jQuery} */ + _$trigger: undefined, + + /** @type {ContactsMenuView} */ + _view: undefined, + + /** @type {Promise} */ + _contactsPromise: undefined, + + /** + * @param {Object} options + * @param {jQuery} options.el - the element to render the menu in + * @param {jQuery} options.trigger - the element to click on to open the menu + * @returns {undefined} + */ + initialize: function(options) { + this.$el = options.el; + this._$trigger = options.trigger; + + this._view = new ContactsMenuView({ + el: this.$el + }); + this._view.on('search', function(searchTerm) { + this._loadContacts(searchTerm); + }, this); + + OC.registerMenu(this._$trigger, this.$el, function() { + this._toggleVisibility(true); + }.bind(this)); + this.$el.on('beforeHide', function() { + this._toggleVisibility(false); + }.bind(this)); + }, + + /** + * @private + * @param {boolean} show + * @returns {Promise} + */ + _toggleVisibility: function(show) { + if (show) { + return this._loadContacts(); + } else { + this.$el.html(''); + return Promise.resolve(); + } + }, + + /** + * @private + * @param {string|undefined} searchTerm + * @returns {Promise} + */ + _getContacts: function(searchTerm) { + var url = OC.generateUrl('/contactsmenu/contacts'); + return Promise.resolve($.ajax(url, { + method: 'GET', + data: { + filter: searchTerm + } + })); + }, + + /** + * @param {string|undefined} searchTerm + * @returns {undefined} + */ + _loadContacts: function(searchTerm) { + var self = this; + + if (!self._contactsPromise) { + self._contactsPromise = self._getContacts(searchTerm); + } + + if (_.isUndefined(searchTerm) || searchTerm === '') { + self._view.showLoading(t('core', 'Loading your contacts …')); + } else { + self._view.showLoading(t('core', 'Looking for {term} …', { + term: searchTerm + })); + } + return self._contactsPromise.then(function(data) { + // Convert contact entries to Backbone collection + data.contacts = new ContactCollection(data.contacts); + + self._view.showContacts(data, searchTerm); + }, function(e) { + self._view.showError(); + console.error('could not load contacts', e); + }).then(function() { + // Delete promise, so that contacts are fetched again when the + // menu is opened the next time. + delete self._contactsPromise; + }).catch(console.error.bind(this)); + } + }; + + OC.ContactsMenu = ContactsMenu; + +})(OC, $, _, Handlebars); diff --git a/core/js/core.json b/core/js/core.json index 6494d4105f8..aadd66a0558 100644 --- a/core/js/core.json +++ b/core/js/core.json @@ -40,6 +40,7 @@ "sharedialogresharerinfoview.js", "sharedialogshareelistview.js", "octemplate.js", + "contactsmenu.js", "eventsource.js", "config.js", "public/appconfig.js", diff --git a/core/js/js.js b/core/js/js.js index 95c00dd6448..03d831567d3 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -654,8 +654,13 @@ var OCP = {}, /** * For menu toggling * @todo Write documentation + * + * @param {jQuery} $toggle + * @param {jQuery} $menuEl + * @param {function|undefined} toggle callback invoked everytime the menu is opened + * @returns {undefined} */ - registerMenu: function($toggle, $menuEl) { + registerMenu: function($toggle, $menuEl, toggle) { var self = this; $menuEl.addClass('menu'); $toggle.on('click.menu', function(event) { @@ -671,7 +676,7 @@ var OCP = {}, // close it self.hideMenus(); } - $menuEl.slideToggle(OC.menuSpeed); + $menuEl.slideToggle(OC.menuSpeed, toggle); OC._currentMenu = $menuEl; OC._currentMenuToggle = $toggle; }); @@ -1473,8 +1478,16 @@ function initCore() { }); } + function setupContactsMenu() { + new OC.ContactsMenu({ + el: $('#contactsmenu .menu'), + trigger: $('#contactsmenu .menutoggle') + }); + } + setupMainMenu(); setupUserMenu(); + setupContactsMenu(); // move triangle of apps dropdown to align with app name triangle // 2 is the additional offset between the triangles diff --git a/core/js/tests/specs/contactsmenuSpec.js b/core/js/tests/specs/contactsmenuSpec.js new file mode 100644 index 00000000000..1db9a6a9554 --- /dev/null +++ b/core/js/tests/specs/contactsmenuSpec.js @@ -0,0 +1,255 @@ +/* global expect, sinon, _, spyOn, Promise */ + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +describe('Contacts menu', function() { + var $triggerEl, + $menuEl, + menu; + + /** + * @private + * @returns {Promise} + */ + function openMenu() { + return menu._toggleVisibility(true); + } + + beforeEach(function(done) { + $triggerEl = $('<div class="menutoggle">'); + $menuEl = $('<div class="menu">'); + + menu = new OC.ContactsMenu({ + el: $menuEl, + trigger: $triggerEl + }); + done(); + }); + + it('shows a loading message while data is being fetched', function() { + fakeServer.respondWith('GET', OC.generateUrl('/contactsmenu/contacts'), [ + 200, + {}, + '' + ]); + + openMenu(); + + expect($menuEl.html()).toContain('Loading your contacts …'); + }); + + it('shows an error message when loading the contacts data fails', function(done) { + spyOn(console, 'error'); + fakeServer.respondWith('GET', OC.generateUrl('/contactsmenu/contacts'), [ + 500, + {}, + '' + ]); + + var opening = openMenu(); + + expect($menuEl.html()).toContain('Loading your contacts …'); + fakeServer.respond(); + + opening.then(function() { + expect($menuEl.html()).toContain('Could not load your contacts.'); + expect(console.error).toHaveBeenCalledTimes(1); + done(); + }, function(e) { + done.fail(e); + }); + }); + + it('loads data successfully', function(done) { + spyOn(menu, '_getContacts').and.returnValue(Promise.resolve({ + contacts: [ + { + id: null, + fullName: 'Acosta Lancaster', + topAction: { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:deboraoliver%40centrexin.com' + }, + actions: [ + { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:mathisholland%40virxo.com' + }, + { + title: 'Details', + icon: 'icon-info', + hyperlink: 'https:\/\/localhost\/index.php\/apps\/contacts' + } + ], + lastMessage: '' + }, + { + id: null, + fullName: 'Adeline Snider', + topAction: { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:ceciliasoto%40essensia.com' + }, + actions: [ + { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:pearliesellers%40inventure.com' + }, + { + title: 'Details', + icon: 'icon-info', + hyperlink: 'https://localhost\/index.php\/apps\/contacts' + } + ], + lastMessage: 'cu' + } + ], + contactsAppEnabled: true + })); + + openMenu().then(function() { + expect(menu._getContacts).toHaveBeenCalled(); + expect($menuEl.html()).toContain('Acosta Lancaster'); + expect($menuEl.html()).toContain('Adeline Snider'); + expect($menuEl.html()).toContain('Show all contacts …'); + done(); + }, function(e) { + done.fail(e); + }); + + }); + + it('doesn\'t show a link to the contacts app if it\'s disabled', function(done) { + spyOn(menu, '_getContacts').and.returnValue(Promise.resolve({ + contacts: [ + { + id: null, + fullName: 'Acosta Lancaster', + topAction: { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:deboraoliver%40centrexin.com' + }, + actions: [ + { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:mathisholland%40virxo.com' + }, + { + title: 'Details', + icon: 'icon-info', + hyperlink: 'https:\/\/localhost\/index.php\/apps\/contacts' + } + ], + lastMessage: '' + } + ], + contactsAppEnabled: false + })); + + openMenu().then(function() { + expect(menu._getContacts).toHaveBeenCalled(); + expect($menuEl.html()).not.toContain('Show all contacts …'); + done(); + }, function(e) { + done.fail(e); + }); + }); + + it('shows only one entry\'s action menu at a time', function(done) { + spyOn(menu, '_getContacts').and.returnValue(Promise.resolve({ + contacts: [ + { + id: null, + fullName: 'Acosta Lancaster', + topAction: { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:deboraoliver%40centrexin.com' + }, + actions: [ + { + title: 'Details', + icon: 'icon-info', + hyperlink: 'https:\/\/localhost\/index.php\/apps\/contacts' + } + ], + lastMessage: '' + }, + { + id: null, + fullName: 'Adeline Snider', + topAction: { + title: 'Mail', + icon: 'icon-mail', + hyperlink: 'mailto:ceciliasoto%40essensia.com' + }, + actions: [ + { + title: 'Details', + icon: 'icon-info', + hyperlink: 'https://localhost\/index.php\/apps\/contacts' + } + ], + lastMessage: 'cu' + } + ], + contactsAppEnabled: true + })); + + openMenu().then(function() { + expect(menu._getContacts).toHaveBeenCalled(); + expect($menuEl.html()).toContain('Adeline Snider'); + expect($menuEl.html()).toContain('Show all contacts …'); + + // Both menus are closed at the beginning + expect($menuEl.find('.contact').eq(0).find('.menu').is(':visible')).toBeFalsy(); + expect($menuEl.find('.contact').eq(1).find('.menu').is(':visible')).toBeFalsy(); + + // Open the first one + $menuEl.find('.contact').eq(0).find('.other-actions').click(); + expect($menuEl.find('.contact').eq(0).find('.menu').css('display')).toBe('block'); + expect($menuEl.find('.contact').eq(1).find('.menu').css('display')).toBe('none'); + + // Open the second one + $menuEl.find('.contact').eq(1).find('.other-actions').click(); + expect($menuEl.find('.contact').eq(0).find('.menu').css('display')).toBe('none'); + expect($menuEl.find('.contact').eq(1).find('.menu').css('display')).toBe('block'); + + // Close the second one + $menuEl.find('.contact').eq(1).find('.other-actions').click(); + expect($menuEl.find('.contact').eq(0).find('.menu').css('display')).toBe('none'); + expect($menuEl.find('.contact').eq(1).find('.menu').css('display')).toBe('none'); + + done(); + }, function(e) { + done.fail(e); + }); + }); + +}); diff --git a/core/routes.php b/core/routes.php index 7a2d9d750cc..0e882c625fc 100644 --- a/core/routes.php +++ b/core/routes.php @@ -57,6 +57,7 @@ $application->registerRoutes($this, [ ['name' => 'Preview#getPreview', 'url' => '/core/preview.png', 'verb' => 'GET'], ['name' => 'Css#getCss', 'url' => '/css/{appName}/{fileName}', 'verb' => 'GET'], ['name' => 'Js#getJs', 'url' => '/js/{appName}/{fileName}', 'verb' => 'GET'], + ['name' => 'contactsMenu#index', 'url' => '/contactsmenu/contacts', 'verb' => 'GET'], ], 'ocs' => [ ['root' => '/cloud', 'name' => 'OCS#getCapabilities', 'url' => '/capabilities', 'verb' => 'GET'], diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php index 91b7eb3490b..426ed4b1125 100644 --- a/core/templates/layout.user.php +++ b/core/templates/layout.user.php @@ -118,6 +118,10 @@ autocomplete="off" tabindex="5"> <button class="icon-close-white" type="reset"></button> </form> + <div id="contactsmenu"> + <div class="icon-contacts menutoggle"></div> + <div class="menu"></div> + </div> <div id="settings"> <div id="expand" tabindex="6" role="link" class="menutoggle"> <div class="avatardiv<?php if ($_['userAvatarSet']) { print_unescaped(' avatardiv-shown'); } else { print_unescaped('" style="display: none'); } ?>"> @@ -161,5 +165,6 @@ <?php print_unescaped($_['content']); ?> </div> </div> + </body> </html> diff --git a/lib/composer/composer/ClassLoader.php b/lib/composer/composer/ClassLoader.php index 2c72175e772..4626994fd4d 100644 --- a/lib/composer/composer/ClassLoader.php +++ b/lib/composer/composer/ClassLoader.php @@ -374,13 +374,9 @@ class ClassLoader $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { - $subPath = $class; - while (false !== $lastPos = strrpos($subPath, '\\')) { - $subPath = substr($subPath, 0, $lastPos); - $search = $subPath.'\\'; - if (isset($this->prefixDirsPsr4[$search])) { - foreach ($this->prefixDirsPsr4[$search] as $dir) { - $length = $this->prefixLengthsPsr4[$first][$search]; + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { return $file; } diff --git a/lib/composer/composer/LICENSE b/lib/composer/composer/LICENSE index f27399a042d..1a28124886d 100644 --- a/lib/composer/composer/LICENSE +++ b/lib/composer/composer/LICENSE @@ -1,5 +1,5 @@ -Copyright (c) Nils Adermann, Jordi Boggiano +Copyright (c) 2016 Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 0d5f067779d..0e858a54aeb 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -80,6 +80,11 @@ return array( 'OCP\\Console\\ConsoleEvent' => $baseDir . '/lib/public/Console/ConsoleEvent.php', 'OCP\\Constants' => $baseDir . '/lib/public/Constants.php', 'OCP\\Contacts' => $baseDir . '/lib/public/Contacts.php', + 'OCP\\Contacts\\ContactsMenu\\IAction' => $baseDir . '/lib/public/Contacts/ContactsMenu/IAction.php', + 'OCP\\Contacts\\ContactsMenu\\IActionFactory' => $baseDir . '/lib/public/Contacts/ContactsMenu/IActionFactory.php', + 'OCP\\Contacts\\ContactsMenu\\IEntry' => $baseDir . '/lib/public/Contacts/ContactsMenu/IEntry.php', + 'OCP\\Contacts\\ContactsMenu\\ILinkAction' => $baseDir . '/lib/public/Contacts/ContactsMenu/ILinkAction.php', + 'OCP\\Contacts\\ContactsMenu\\IProvider' => $baseDir . '/lib/public/Contacts/ContactsMenu/IProvider.php', 'OCP\\Contacts\\IManager' => $baseDir . '/lib/public/Contacts/IManager.php', 'OCP\\DB' => $baseDir . '/lib/public/DB.php', 'OCP\\DB\\QueryBuilder\\ICompositeExpression' => $baseDir . '/lib/public/DB/QueryBuilder/ICompositeExpression.php', @@ -373,6 +378,14 @@ return array( 'OC\\Console\\Application' => $baseDir . '/lib/private/Console/Application.php', 'OC\\Console\\TimestampFormatter' => $baseDir . '/lib/private/Console/TimestampFormatter.php', 'OC\\ContactsManager' => $baseDir . '/lib/private/ContactsManager.php', + 'OC\\Contacts\\ContactsMenu\\ActionFactory' => $baseDir . '/lib/private/Contacts/ContactsMenu/ActionFactory.php', + 'OC\\Contacts\\ContactsMenu\\ActionProviderStore' => $baseDir . '/lib/private/Contacts/ContactsMenu/ActionProviderStore.php', + 'OC\\Contacts\\ContactsMenu\\Actions\\LinkAction' => $baseDir . '/lib/private/Contacts/ContactsMenu/Actions/LinkAction.php', + 'OC\\Contacts\\ContactsMenu\\ContactsStore' => $baseDir . '/lib/private/Contacts/ContactsMenu/ContactsStore.php', + 'OC\\Contacts\\ContactsMenu\\Entry' => $baseDir . '/lib/private/Contacts/ContactsMenu/Entry.php', + 'OC\\Contacts\\ContactsMenu\\Manager' => $baseDir . '/lib/private/Contacts/ContactsMenu/Manager.php', + 'OC\\Contacts\\ContactsMenu\\Providers\\DetailsProvider' => $baseDir . '/lib/private/Contacts/ContactsMenu/Providers/DetailsProvider.php', + 'OC\\Contacts\\ContactsMenu\\Providers\\EMailProvider' => $baseDir . '/lib/private/Contacts/ContactsMenu/Providers/EMailProvider.php', 'OC\\Core\\Application' => $baseDir . '/core/Application.php', 'OC\\Core\\Command\\App\\CheckCode' => $baseDir . '/core/Command/App/CheckCode.php', 'OC\\Core\\Command\\App\\Disable' => $baseDir . '/core/Command/App/Disable.php', @@ -444,6 +457,7 @@ return array( 'OC\\Core\\Command\\User\\ResetPassword' => $baseDir . '/core/Command/User/ResetPassword.php', 'OC\\Core\\Command\\User\\Setting' => $baseDir . '/core/Command/User/Setting.php', 'OC\\Core\\Controller\\AvatarController' => $baseDir . '/core/Controller/AvatarController.php', + 'OC\\Core\\Controller\\ContactsMenuController' => $baseDir . '/core/Controller/ContactsMenuController.php', 'OC\\Core\\Controller\\CssController' => $baseDir . '/core/Controller/CssController.php', 'OC\\Core\\Controller\\JsController' => $baseDir . '/core/Controller/JsController.php', 'OC\\Core\\Controller\\LoginController' => $baseDir . '/core/Controller/LoginController.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 82c31c24a21..73fba39b23a 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -110,6 +110,11 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Console\\ConsoleEvent' => __DIR__ . '/../../..' . '/lib/public/Console/ConsoleEvent.php', 'OCP\\Constants' => __DIR__ . '/../../..' . '/lib/public/Constants.php', 'OCP\\Contacts' => __DIR__ . '/../../..' . '/lib/public/Contacts.php', + 'OCP\\Contacts\\ContactsMenu\\IAction' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IAction.php', + 'OCP\\Contacts\\ContactsMenu\\IActionFactory' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IActionFactory.php', + 'OCP\\Contacts\\ContactsMenu\\IEntry' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IEntry.php', + 'OCP\\Contacts\\ContactsMenu\\ILinkAction' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/ILinkAction.php', + 'OCP\\Contacts\\ContactsMenu\\IProvider' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IProvider.php', 'OCP\\Contacts\\IManager' => __DIR__ . '/../../..' . '/lib/public/Contacts/IManager.php', 'OCP\\DB' => __DIR__ . '/../../..' . '/lib/public/DB.php', 'OCP\\DB\\QueryBuilder\\ICompositeExpression' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/ICompositeExpression.php', @@ -403,6 +408,14 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Console\\Application' => __DIR__ . '/../../..' . '/lib/private/Console/Application.php', 'OC\\Console\\TimestampFormatter' => __DIR__ . '/../../..' . '/lib/private/Console/TimestampFormatter.php', 'OC\\ContactsManager' => __DIR__ . '/../../..' . '/lib/private/ContactsManager.php', + 'OC\\Contacts\\ContactsMenu\\ActionFactory' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/ActionFactory.php', + 'OC\\Contacts\\ContactsMenu\\ActionProviderStore' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/ActionProviderStore.php', + 'OC\\Contacts\\ContactsMenu\\Actions\\LinkAction' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Actions/LinkAction.php', + 'OC\\Contacts\\ContactsMenu\\ContactsStore' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/ContactsStore.php', + 'OC\\Contacts\\ContactsMenu\\Entry' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Entry.php', + 'OC\\Contacts\\ContactsMenu\\Manager' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Manager.php', + 'OC\\Contacts\\ContactsMenu\\Providers\\DetailsProvider' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Providers/DetailsProvider.php', + 'OC\\Contacts\\ContactsMenu\\Providers\\EMailProvider' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Providers/EMailProvider.php', 'OC\\Core\\Application' => __DIR__ . '/../../..' . '/core/Application.php', 'OC\\Core\\Command\\App\\CheckCode' => __DIR__ . '/../../..' . '/core/Command/App/CheckCode.php', 'OC\\Core\\Command\\App\\Disable' => __DIR__ . '/../../..' . '/core/Command/App/Disable.php', @@ -474,6 +487,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Command\\User\\ResetPassword' => __DIR__ . '/../../..' . '/core/Command/User/ResetPassword.php', 'OC\\Core\\Command\\User\\Setting' => __DIR__ . '/../../..' . '/core/Command/User/Setting.php', 'OC\\Core\\Controller\\AvatarController' => __DIR__ . '/../../..' . '/core/Controller/AvatarController.php', + 'OC\\Core\\Controller\\ContactsMenuController' => __DIR__ . '/../../..' . '/core/Controller/ContactsMenuController.php', 'OC\\Core\\Controller\\CssController' => __DIR__ . '/../../..' . '/core/Controller/CssController.php', 'OC\\Core\\Controller\\JsController' => __DIR__ . '/../../..' . '/core/Controller/JsController.php', 'OC\\Core\\Controller\\LoginController' => __DIR__ . '/../../..' . '/core/Controller/LoginController.php', diff --git a/lib/private/Contacts/ContactsMenu/ActionFactory.php b/lib/private/Contacts/ContactsMenu/ActionFactory.php new file mode 100644 index 00000000000..1d2a69c904d --- /dev/null +++ b/lib/private/Contacts/ContactsMenu/ActionFactory.php @@ -0,0 +1,57 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Contacts\ContactsMenu; + +use OC\Contacts\ContactsMenu\Actions\LinkAction; +use OCP\Contacts\ContactsMenu\IActionFactory; +use OCP\Contacts\ContactsMenu\ILinkAction; + +class ActionFactory implements IActionFactory { + + /** + * @param string $icon + * @param string $name + * @param string $href + * @return ILinkAction + */ + public function newLinkAction($icon, $name, $href) { + $action = new LinkAction(); + $action->setName($name); + $action->setIcon($icon); + $action->setHref($href); + return $action; + } + + /** + * @param string $icon + * @param string $name + * @param string $email + * @return ILinkAction + */ + public function newEMailAction($icon, $name, $email) { + return $this->newLinkAction($icon, $name, 'mailto:' . urlencode($email)); + } + +} diff --git a/lib/private/Contacts/ContactsMenu/ActionProviderStore.php b/lib/private/Contacts/ContactsMenu/ActionProviderStore.php new file mode 100644 index 00000000000..cc5996377dd --- /dev/null +++ b/lib/private/Contacts/ContactsMenu/ActionProviderStore.php @@ -0,0 +1,82 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Contacts\ContactsMenu; + +use OC\Contacts\ContactsMenu\Providers\EMailProvider; +use OCP\AppFramework\QueryException; +use OCP\Contacts\ContactsMenu\IProvider; +use OCP\ILogger; +use OCP\IServerContainer; + +class ActionProviderStore { + + /** @var IServerContainer */ + private $serverContainer; + + /** @var ILogger */ + private $logger; + + /** + * @param IServerContainer $serverContainer + */ + public function __construct(IServerContainer $serverContainer, ILogger $logger) { + $this->serverContainer = $serverContainer; + $this->logger = $logger; + } + + /** + * @return IProvider[] + * @throws Exception + */ + public function getProviders() { + // TODO: include apps + $providerClasses = $this->getServerProviderClasses(); + $providers = []; + + foreach ($providerClasses as $class) { + try { + $providers[] = $this->serverContainer->query($class); + } catch (QueryException $ex) { + $this->logger->logException($ex, [ + 'message' => "Could not load contacts menu action provider $class", + 'app' => 'core', + ]); + throw new \Exception("Could not load contacts menu action provider"); + } + } + + return $providers; + } + + /** + * @return string[] + */ + private function getServerProviderClasses() { + return [ + EMailProvider::class, + ]; + } + +} diff --git a/lib/private/Contacts/ContactsMenu/Actions/LinkAction.php b/lib/private/Contacts/ContactsMenu/Actions/LinkAction.php new file mode 100644 index 00000000000..5b8b0524a21 --- /dev/null +++ b/lib/private/Contacts/ContactsMenu/Actions/LinkAction.php @@ -0,0 +1,103 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Contacts\ContactsMenu\Actions; + +use OCP\Contacts\ContactsMenu\ILinkAction; + +class LinkAction implements ILinkAction { + + /** @var string */ + private $icon; + + /** @var string */ + private $name; + + /** @var string */ + private $href; + + /** @var int */ + private $priority = 10; + + /** + * @param string $icon absolute URI to an icon + */ + public function setIcon($icon) { + $this->icon = $icon; + } + + /** + * @param string $name + */ + public function setName($name) { + $this->name = $name; + } + + /** + * @return string + */ + public function getName() { + return $this->name; + } + + /** + * @param int $priority + */ + public function setPriority($priority) { + $this->priority = $priority; + } + + /** + * @return int + */ + public function getPriority() { + return $this->priority; + } + + /** + * @param string $href + */ + public function setHref($href) { + $this->href = $href; + } + + /** + * @return string + */ + public function getHref() { + return $this->href; + } + + /** + * @return array + */ + public function jsonSerialize() { + return [ + 'title' => $this->name, + 'icon' => $this->icon, + 'hyperlink' => $this->href, + ]; + } + +} diff --git a/lib/private/Contacts/ContactsMenu/ContactsStore.php b/lib/private/Contacts/ContactsMenu/ContactsStore.php new file mode 100644 index 00000000000..fba970e93fa --- /dev/null +++ b/lib/private/Contacts/ContactsMenu/ContactsStore.php @@ -0,0 +1,89 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Contacts\ContactsMenu; + +use OCP\Contacts\ContactsMenu\IEntry; +use OCP\Contacts\IManager; + +class ContactsStore { + + /** @var IManager */ + private $contactsManager; + + /** + * @param IManager $contactsManager + */ + public function __construct(IManager $contactsManager) { + $this->contactsManager = $contactsManager; + } + + /** + * @param string|null $filter + * @return IEntry[] + */ + public function getContacts($filter) { + $allContacts = $this->contactsManager->search($filter ?: '', [ + 'FN', + ]); + + return array_map(function(array $contact) { + return $this->contactArrayToEntry($contact); + }, $allContacts); + } + + /** + * @param array $contact + * @return Entry + */ + private function contactArrayToEntry(array $contact) { + $entry = new Entry(); + + if (isset($contact['id'])) { + $entry->setId($contact['id']); + } + + if (isset($contact['FN'])) { + $entry->setFullName($contact['FN']); + } + + $avatarPrefix = "VALUE=uri:"; + if (isset($contact['PHOTO']) && strpos($contact['PHOTO'], $avatarPrefix) === 0) { + $entry->setAvatar(substr($contact['PHOTO'], strlen($avatarPrefix))); + } + + if (isset($contact['EMAIL'])) { + foreach ($contact['EMAIL'] as $email) { + $entry->addEMailAddress($email); + } + } + + // Attach all other properties to the entry too because some + // providers might make use of it. + $entry->setProperties($contact); + + return $entry; + } + +} diff --git a/lib/private/Contacts/ContactsMenu/Entry.php b/lib/private/Contacts/ContactsMenu/Entry.php new file mode 100644 index 00000000000..9ea0511b9cc --- /dev/null +++ b/lib/private/Contacts/ContactsMenu/Entry.php @@ -0,0 +1,169 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Contacts\ContactsMenu; + +use OCP\Contacts\ContactsMenu\IAction; +use OCP\Contacts\ContactsMenu\IEntry; + +class Entry implements IEntry { + + /** @var string|int|null */ + private $id = null; + + /** @var string */ + private $fullName = ''; + + /** @var string[] */ + private $emailAddresses = []; + + /** @var string|null */ + private $avatar; + + /** @var IAction[] */ + private $actions = []; + + /** @var array */ + private $properties = []; + + /** + * @param string $id + */ + public function setId($id) { + $this->id = $id; + } + + /** + * @param string $displayName + */ + public function setFullName($displayName) { + $this->fullName = $displayName; + } + + /** + * @return string + */ + public function getFullName() { + return $this->fullName; + } + + /** + * @param string $address + */ + public function addEMailAddress($address) { + $this->emailAddresses[] = $address; + } + + /** + * @return string + */ + public function getEMailAddresses() { + return $this->emailAddresses; + } + + /** + * @param string $avatar + */ + public function setAvatar($avatar) { + $this->avatar = $avatar; + } + + /** + * @return string + */ + public function getAvatar() { + return $this->avatar; + } + + /** + * @param IAction $action + */ + public function addAction(IAction $action) { + $this->actions[] = $action; + $this->sortActions(); + } + + /** + * @return IAction[] + */ + public function getActions() { + return $this->actions; + } + + /** + * sort the actions by priority and name + */ + private function sortActions() { + usort($this->actions, function(IAction $action1, IAction $action2) { + $prio1 = $action1->getPriority(); + $prio2 = $action2->getPriority(); + + if ($prio1 === $prio2) { + // Ascending order for same priority + return strcasecmp($action1->getName(), $action2->getName()); + } + + // Descending order when priority differs + return $prio2 - $prio1; + }); + } + + /** + * @param array $contact key-value array containing additional properties + */ + public function setProperties(array $contact) { + $this->properties = $contact; + } + + /** + * @param string $key + * @return mixed + */ + public function getProperty($key) { + if (!isset($this->properties[$key])) { + return null; + } + return $this->properties[$key]; + } + + /** + * @return array + */ + public function jsonSerialize() { + $topAction = !empty($this->actions) ? $this->actions[0]->jsonSerialize() : null; + $otherActions = array_map(function(IAction $action) { + return $action->jsonSerialize(); + }, array_slice($this->actions, 1)); + + return [ + 'id' => $this->id, + 'fullName' => $this->fullName, + 'avatar' => $this->getAvatar(), + 'topAction' => $topAction, + 'actions' => $otherActions, + 'lastMessage' => '', + ]; + } + +} diff --git a/lib/private/Contacts/ContactsMenu/Manager.php b/lib/private/Contacts/ContactsMenu/Manager.php new file mode 100644 index 00000000000..c759c4469da --- /dev/null +++ b/lib/private/Contacts/ContactsMenu/Manager.php @@ -0,0 +1,98 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Contacts\ContactsMenu; + +use OCP\App\IAppManager; +use OCP\Contacts\ContactsMenu\IEntry; +use OCP\IURLGenerator; + +class Manager { + + /** @var ContactsStore */ + private $store; + + /** @var ActionProviderStore */ + private $actionProviderStore; + + /** @var IAppManager */ + private $appManager; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** + * @param ContactsStore $store + * @param ActionProviderStore $actionProviderStore + * @param IAppManager $appManager + */ + public function __construct(ContactsStore $store, ActionProviderStore $actionProviderStore, IAppManager $appManager) { + $this->store = $store; + $this->actionProviderStore = $actionProviderStore; + $this->appManager = $appManager; + } + + /** + * @param string $userId + * @param string $filter + * @return array + */ + public function getEntries($userId, $filter) { + $entries = $this->store->getContacts($filter); + + $sortedEntries = $this->sortEntries($entries); + $topEntries = array_slice($sortedEntries, 0, 25); + $this->processEntries($topEntries); + + $contactsEnabled = $this->appManager->isEnabledForUser('contacts', $userId); + return [ + 'contacts' => $topEntries, + 'contactsAppEnabled' => $contactsEnabled, + ]; + } + + /** + * @param IEntry[] $entries + * @return IEntry[] + */ + private function sortEntries(array $entries) { + usort($entries, function(IEntry $entryA, IEntry $entryB) { + return strcasecmp($entryA->getFullName(), $entryB->getFullName()); + }); + return $entries; + } + + /** + * @param IEntry[] $entries + */ + private function processEntries(array $entries) { + $providers = $this->actionProviderStore->getProviders(); + foreach ($entries as $entry) { + foreach ($providers as $provider) { + $provider->process($entry); + } + } + } + +} diff --git a/lib/private/Contacts/ContactsMenu/Providers/EMailProvider.php b/lib/private/Contacts/ContactsMenu/Providers/EMailProvider.php new file mode 100644 index 00000000000..c74bdc0b8ff --- /dev/null +++ b/lib/private/Contacts/ContactsMenu/Providers/EMailProvider.php @@ -0,0 +1,56 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Contacts\ContactsMenu\Providers; + +use OCP\Contacts\ContactsMenu\IActionFactory; +use OCP\Contacts\ContactsMenu\IEntry; +use OCP\Contacts\ContactsMenu\IProvider; + +class EMailProvider implements IProvider { + + /** @var IActionFactory */ + private $actionFactory; + + /** + * @param IActionFactory $actionFactory + */ + public function __construct(IActionFactory $actionFactory) { + $this->actionFactory = $actionFactory; + } + + /** + * @param IEntry $entry + */ + public function process(IEntry $entry) { + foreach ($entry->getEMailAddresses() as $address) { + // TODO: absolute path + // TODO: meaningful URL + // TODO: l10n + $action = $this->actionFactory->newEMailAction('icon-mail', 'Mail', $address); + $entry->addAction($action); + } + } + +} diff --git a/lib/private/Server.php b/lib/private/Server.php index f40a59ad334..7724feb551b 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -50,6 +50,7 @@ use OC\AppFramework\Utility\SimpleContainer; use OC\AppFramework\Utility\TimeFactory; use OC\Authentication\LoginCredentials\Store; use OC\Command\AsyncBus; +use OC\Contacts\ContactsMenu\ActionFactory; use OC\Diagnostics\EventLogger; use OC\Diagnostics\NullEventLogger; use OC\Diagnostics\NullQueryLogger; @@ -108,6 +109,8 @@ use OCP\IDBConnection; use OCP\IL10N; use OCP\IServerContainer; use OCP\ITempManager; +use OCP\Contacts\ContactsMenu\IActionFactory; +use OCP\IURLGenerator; use OCP\RichObjectStrings\IValidator; use OCP\Security\IContentSecurityPolicyManager; use OCP\Share\IShareHelper; @@ -133,9 +136,17 @@ class Server extends ServerContainer implements IServerContainer { parent::__construct(); $this->webRoot = $webRoot; + $this->registerService(\OCP\IServerContainer::class, function(IServerContainer $c) { + return $c; + }); + $this->registerAlias(\OCP\Contacts\IManager::class, \OC\ContactsManager::class); $this->registerAlias('ContactsManager', \OCP\Contacts\IManager::class); + $this->registerAlias(IActionFactory::class, ActionFactory::class); + + + $this->registerService(\OCP\IPreview::class, function (Server $c) { return new PreviewManager( $c->getConfig(), diff --git a/lib/private/legacy/template.php b/lib/private/legacy/template.php index 19b5e418110..9a919ff12f2 100644 --- a/lib/private/legacy/template.php +++ b/lib/private/legacy/template.php @@ -118,6 +118,7 @@ class OC_Template extends \OC\Template\Base { OC_Util::addScript('jquery-ui-fixes'); OC_Util::addScript('files/fileinfo'); OC_Util::addScript('files/client'); + OC_Util::addScript('contactsmenu'); if (\OC::$server->getConfig()->getSystemValue('debug')) { // Add the stuff we need always diff --git a/lib/public/Contacts/ContactsMenu/IAction.php b/lib/public/Contacts/ContactsMenu/IAction.php new file mode 100644 index 00000000000..44ad1af5ae8 --- /dev/null +++ b/lib/public/Contacts/ContactsMenu/IAction.php @@ -0,0 +1,65 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Contacts\ContactsMenu; + +use JsonSerializable; + +/** + * Apps should use the IActionFactory to create new action objects + * + * @since 12.0 + */ +interface IAction extends JsonSerializable { + + /** + * @param string $icon absolute URI to an icon + * @since 12.0 + */ + public function setIcon($icon); + + /** + * @return string localized action name, e.g. 'Call' + * @since 12.0 + */ + public function getName(); + + /** + * @param string $name localized action name, e.g. 'Call' + * @since 12.0 + */ + public function setName($name); + + /** + * @param int $priority priorize actions, high order ones are shown on top + * @since 12.0 + */ + public function setPriority($priority); + + /** + * @return int priority to priorize actions, high order ones are shown on top + * @since 12.0 + */ + public function getPriority(); +} diff --git a/lib/public/Contacts/ContactsMenu/IActionFactory.php b/lib/public/Contacts/ContactsMenu/IActionFactory.php new file mode 100644 index 00000000000..8778a729a56 --- /dev/null +++ b/lib/public/Contacts/ContactsMenu/IActionFactory.php @@ -0,0 +1,54 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Contacts\ContactsMenu; + +/** + * @since 12.0 + */ +interface IActionFactory { + + /** + * Construct and return a new link action for the contacts menu + * + * @since 12.0 + * + * @param string $icon full path to the action's icon + * @param string $name localized name of the action + * @param string $href target URL + * @return ILinkAction + */ + public function newLinkAction($icon, $name, $href); + + /** + * Construct and return a new email action for the contacts menu + * + * @since 12.0 + * + * @param string $icon full path to the action's icon + * @param string $name localized name of the action + * @param string $email target e-mail address + * @return ILinkAction + */ + public function newEMailAction($icon, $name, $email); +} diff --git a/lib/public/Contacts/ContactsMenu/IEntry.php b/lib/public/Contacts/ContactsMenu/IEntry.php new file mode 100644 index 00000000000..eb04147a1bc --- /dev/null +++ b/lib/public/Contacts/ContactsMenu/IEntry.php @@ -0,0 +1,66 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Contacts\ContactsMenu; + +use JsonSerializable; + +/** + * @since 12.0 + */ +interface IEntry extends JsonSerializable { + + /** + * @since 12.0 + * @return string + */ + public function getFullName(); + + /** + * @since 12.0 + * @return string[] + */ + public function getEMailAddresses(); + + /** + * @since 12.0 + * @return string|null image URI + */ + public function getAvatar(); + + /** + * @since 12.0 + * @param IAction $action an action to show in the contacts menu + */ + public function addAction(IAction $action); + + /** + * Get an arbitrary property from the contact + * + * @since 12.0 + * @param string $key + * @return mixed the value of the property or null + */ + public function getProperty($key); +} diff --git a/lib/public/Contacts/ContactsMenu/ILinkAction.php b/lib/public/Contacts/ContactsMenu/ILinkAction.php new file mode 100644 index 00000000000..4e29f757c26 --- /dev/null +++ b/lib/public/Contacts/ContactsMenu/ILinkAction.php @@ -0,0 +1,43 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Contacts\ContactsMenu; + +/** + * @since 12.0 + */ +interface ILinkAction extends IAction { + + /** + * @since 12.0 + * @param string $href the target URL of the action + */ + public function setHref($href); + + /** + * @since 12.0 + * @return string + */ + public function getHref(); +} diff --git a/lib/public/Contacts/ContactsMenu/IProvider.php b/lib/public/Contacts/ContactsMenu/IProvider.php new file mode 100644 index 00000000000..17fcc003720 --- /dev/null +++ b/lib/public/Contacts/ContactsMenu/IProvider.php @@ -0,0 +1,37 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Contacts\ContactsMenu; + +/** + * @since 12.0 + */ +interface IProvider { + + /** + * @since 12.0 + * @param IEntry $entry + */ + public function process(IEntry $entry); +} diff --git a/tests/Core/Controller/ContactsMenuControllerTest.php b/tests/Core/Controller/ContactsMenuControllerTest.php new file mode 100644 index 00000000000..2483d5e94c3 --- /dev/null +++ b/tests/Core/Controller/ContactsMenuControllerTest.php @@ -0,0 +1,73 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Tests\Controller; + +use OC\Contacts\ContactsMenu\Manager; +use OC\Core\Controller\ContactsMenuController; +use OCP\Contacts\ContactsMenu\IEntry; +use OCP\IRequest; +use PHPUnit_Framework_MockObject_MockObject; +use Test\TestCase; + +class ContactsMenuControllerTest extends TestCase { + + /** @var IRequest|PHPUnit_Framework_MockObject_MockObject */ + private $request; + + /** @var string */ + private $userId; + + /** @var Manager|PHPUnit_Framework_MockObject_MockObject */ + private $contactsManager; + + /** @var ContactsMenuController */ + private $controller; + + protected function setUp() { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->userId = 'user4563'; + $this->contactsManager = $this->createMock(Manager::class); + + $this->controller = new ContactsMenuController($this->request, $this->userId, $this->contactsManager); + } + + public function testIndex() { + $entries = [ + $this->createMock(IEntry::class), + $this->createMock(IEntry::class), + ]; + $this->contactsManager->expects($this->once()) + ->method('getEntries') + ->with($this->equalTo($this->userId), $this->equalTo(null)) + ->willReturn($entries); + + $response = $this->controller->index(); + + $this->assertEquals($entries, $response); + } + +} diff --git a/tests/lib/Contacts/ContactsMenu/ActionFactoryTest.php b/tests/lib/Contacts/ContactsMenu/ActionFactoryTest.php new file mode 100644 index 00000000000..d1273c2b9ad --- /dev/null +++ b/tests/lib/Contacts/ContactsMenu/ActionFactoryTest.php @@ -0,0 +1,67 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Tests\Contacts\ContactsMenu; + +use OC\Contacts\ContactsMenu\ActionFactory; +use OCP\Contacts\ContactsMenu\IAction; +use Test\TestCase; + +class ActionFactoryTest extends TestCase { + + /** @var ActionFactory */ + private $actionFactory; + + protected function setUp() { + parent::setUp(); + + $this->actionFactory = new ActionFactory(); + } + + public function testNewLinkAction() { + $icon = 'icon-test'; + $name = 'Test'; + $href = 'some/url'; + + $action = $this->actionFactory->newLinkAction($icon, $name, $href); + + $this->assertInstanceOf(IAction::class, $action); + $this->assertEquals($name, $action->getName()); + $this->assertEquals(10, $action->getPriority()); + } + + public function testNewEMailAction() { + $icon = 'icon-test'; + $name = 'Test'; + $href = 'user@example.com'; + + $action = $this->actionFactory->newEMailAction($icon, $name, $href); + + $this->assertInstanceOf(IAction::class, $action); + $this->assertEquals($name, $action->getName()); + $this->assertEquals(10, $action->getPriority()); + $this->assertEquals('mailto:user%40example.com', $action->getHref()); + } + +} diff --git a/tests/lib/Contacts/ContactsMenu/ActionProviderStoreTest.php b/tests/lib/Contacts/ContactsMenu/ActionProviderStoreTest.php new file mode 100644 index 00000000000..208ce94c6b0 --- /dev/null +++ b/tests/lib/Contacts/ContactsMenu/ActionProviderStoreTest.php @@ -0,0 +1,82 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Tests\Contacts\ContactsMenu; + +use Exception; +use OC\Contacts\ContactsMenu\ActionProviderStore; +use OC\Contacts\ContactsMenu\Providers\EMailProvider; +use OCP\AppFramework\QueryException; +use OCP\ILogger; +use OCP\IServerContainer; +use PHPUnit_Framework_MockObject_MockObject; +use Test\TestCase; + +class ActionProviderStoreTest extends TestCase { + + /** @var IServerContainer|PHPUnit_Framework_MockObject_MockObject */ + private $serverContainer; + + /** @var ILogger|PHPUnit_Framework_MockObject_MockObject */ + private $logger; + + /** @var ActionProviderStore */ + private $actionProviderStore; + + protected function setUp() { + parent::setUp(); + + $this->serverContainer = $this->createMock(IServerContainer::class); + $this->logger = $this->createMock(ILogger::class); + $this->actionProviderStore = new ActionProviderStore($this->serverContainer, $this->logger); + } + + public function testGetProviders() { + $emailProvider = $this->createMock(EMailProvider::class); + $this->serverContainer->expects($this->exactly(2)) + ->method('query') + ->will($this->returnValueMap([ + [EMailProvider::class, $emailProvider], + ])); + + $providers = $this->actionProviderStore->getProviders(); + + $this->assertCount(1, $providers); + $this->assertInstanceOf(EMailProvider::class, $providers[0]); + } + + /** + * @expectedException Exception + */ + public function testGetProvidersWithQueryException() { + $emailProvider = $this->createMock(EMailProvider::class); + $detailsProvider = $this->createMock(DetailsProvider::class); + $this->serverContainer->expects($this->once()) + ->method('query') + ->willThrowException(new QueryException()); + + $providers = $this->actionProviderStore->getProviders(); + } + +} diff --git a/tests/lib/Contacts/ContactsMenu/Actions/LinkActionTest.php b/tests/lib/Contacts/ContactsMenu/Actions/LinkActionTest.php new file mode 100644 index 00000000000..31654b40918 --- /dev/null +++ b/tests/lib/Contacts/ContactsMenu/Actions/LinkActionTest.php @@ -0,0 +1,90 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Tests\Contacts\ContactsMenu\Actions; + +use OC\Contacts\ContactsMenu\Actions\LinkAction; +use Test\TestCase; + +class LinkActionTest extends TestCase { + + private $action; + + protected function setUp() { + parent::setUp(); + + $this->action = new LinkAction(); + } + + public function testSetIcon() { + $icon = 'icon-test'; + + $this->action->setIcon($icon); + $json = $this->action->jsonSerialize(); + + $this->assertArrayHasKey('icon', $json); + $this->assertEquals($json['icon'], $icon); + } + + public function testGetSetName() { + $name = 'Jane Doe'; + + $this->assertNull($this->action->getName()); + $this->action->setName($name); + $this->assertEquals($name, $this->action->getName()); + } + + public function testGetSetPriority() { + $prio = 50; + + $this->assertEquals(10, $this->action->getPriority()); + $this->action->setPriority($prio); + $this->assertEquals($prio, $this->action->getPriority()); + } + + public function testSetHref() { + $this->action->setHref('/some/url'); + + $json = $this->action->jsonSerialize(); + $this->assertArrayHasKey('hyperlink', $json); + $this->assertEquals($json['hyperlink'], '/some/url'); + } + + public function testJsonSerialize() { + $this->action->setIcon('icon-contacts'); + $this->action->setName('Nickie Works'); + $this->action->setPriority(33); + $this->action->setHref('example.com'); + $expected = [ + 'title' => 'Nickie Works', + 'icon' => 'icon-contacts', + 'hyperlink' => 'example.com', + ]; + + $json = $this->action->jsonSerialize(); + + $this->assertEquals($expected, $json); + } + +} diff --git a/tests/lib/Contacts/ContactsMenu/ContactsStoreTest.php b/tests/lib/Contacts/ContactsMenu/ContactsStoreTest.php new file mode 100644 index 00000000000..fa7c57bb409 --- /dev/null +++ b/tests/lib/Contacts/ContactsMenu/ContactsStoreTest.php @@ -0,0 +1,121 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Tests\Contacts\ContactsMenu; + +use OC\Contacts\ContactsMenu\ContactsStore; +use OCP\Contacts\IManager; +use PHPUnit_Framework_MockObject_MockObject; +use Test\TestCase; + +class ContactsStoreTest extends TestCase { + + /** @var ContactsStore */ + private $contactsStore; + + /** @var IManager|PHPUnit_Framework_MockObject_MockObject */ + private $contactsManager; + + protected function setUp() { + parent::setUp(); + + $this->contactsManager = $this->createMock(IManager::class); + + $this->contactsStore = new ContactsStore($this->contactsManager); + } + + public function testGetContactsWithoutFilter() { + $this->contactsManager->expects($this->once()) + ->method('search') + ->with($this->equalTo(''), $this->equalTo(['FN'])) + ->willReturn([ + [ + 'id' => 123, + ], + [ + 'id' => 567, + 'FN' => 'Darren Roner', + 'EMAIL' => [ + 'darren@roner.au' + ], + ], + ]); + + $entries = $this->contactsStore->getContacts(''); + + $this->assertCount(2, $entries); + $this->assertEquals([ + 'darren@roner.au' + ], $entries[1]->getEMailAddresses()); + } + + public function testGetContactsWithoutBinaryImage() { + $this->contactsManager->expects($this->once()) + ->method('search') + ->with($this->equalTo(''), $this->equalTo(['FN'])) + ->willReturn([ + [ + 'id' => 123, + ], + [ + 'id' => 567, + 'FN' => 'Darren Roner', + 'EMAIL' => [ + 'darren@roner.au' + ], + 'PHOTO' => base64_encode('photophotophoto'), + ], + ]); + + $entries = $this->contactsStore->getContacts(''); + + $this->assertCount(2, $entries); + $this->assertNull($entries[1]->getAvatar()); + } + + public function testGetContactsWithoutAvatarURI() { + $this->contactsManager->expects($this->once()) + ->method('search') + ->with($this->equalTo(''), $this->equalTo(['FN'])) + ->willReturn([ + [ + 'id' => 123, + ], + [ + 'id' => 567, + 'FN' => 'Darren Roner', + 'EMAIL' => [ + 'darren@roner.au' + ], + 'PHOTO' => 'VALUE=uri:https://photo', + ], + ]); + + $entries = $this->contactsStore->getContacts(''); + + $this->assertCount(2, $entries); + $this->assertEquals('https://photo', $entries[1]->getAvatar()); + } + +} diff --git a/tests/lib/Contacts/ContactsMenu/EntryTest.php b/tests/lib/Contacts/ContactsMenu/EntryTest.php new file mode 100644 index 00000000000..fa34916f47f --- /dev/null +++ b/tests/lib/Contacts/ContactsMenu/EntryTest.php @@ -0,0 +1,113 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Tests\Contacts\ContactsMenu; + +use OC\Contacts\ContactsMenu\Actions\LinkAction; +use OC\Contacts\ContactsMenu\Entry; +use OCP\Contacts\ContactsMenu\IAction; +use Test\TestCase; + +class EntryTest extends \PHPUnit_Framework_TestCase { + + /** @var Entry */ + private $entry; + + protected function setUp() { + parent::setUp(); + + $this->entry = new Entry(); + } + + public function testSetId() { + $this->entry->setId(123); + } + + public function testSetGetFullName() { + $fn = 'Danette Chaille'; + $this->assertEquals('', $this->entry->getFullName()); + $this->entry->setFullName($fn); + $this->assertEquals($fn, $this->entry->getFullName()); + } + + public function testAddGetEMailAddresses() { + $this->assertEmpty($this->entry->getEMailAddresses()); + $this->entry->addEMailAddress('user@example.com'); + $this->assertEquals(['user@example.com'], $this->entry->getEMailAddresses()); + } + + public function testAddAndSortAction() { + // Three actions, two with equal priority + $action1 = new LinkAction(); + $action2 = new LinkAction(); + $action3 = new LinkAction(); + $action1->setPriority(10); + $action1->setName('Bravo'); + + $action2->setPriority(0); + $action2->setName('Batman'); + + $action3->setPriority(10); + $action3->setName('Alfa'); + + $this->entry->addAction($action1); + $this->entry->addAction($action2); + $this->entry->addAction($action3); + $sorted = $this->entry->getActions(); + + $this->assertSame($action3, $sorted[0]); + $this->assertSame($action1, $sorted[1]); + $this->assertSame($action2, $sorted[2]); + } + + public function testSetGetProperties() { + $props = [ + 'prop1' => 123, + 'prop2' => 'string', + ]; + + $this->entry->setProperties($props); + + $this->assertNull($this->entry->getProperty('doesntexist')); + $this->assertEquals(123, $this->entry->getProperty('prop1')); + $this->assertEquals('string', $this->entry->getProperty('prop2')); + } + + public function testJsonSerialize() { + $expectedJson = [ + 'id' => 123, + 'fullName' => 'Guadalupe Frisbey', + 'topAction' => null, + 'actions' => [], + 'lastMessage' => '', + ]; + + $this->entry->setId(123); + $this->entry->setFullName('Guadalupe Frisbey'); + $json = $this->entry->jsonSerialize(); + + $this->assertEquals($expectedJson, $json); + } + +} diff --git a/tests/lib/Contacts/ContactsMenu/ManagerTest.php b/tests/lib/Contacts/ContactsMenu/ManagerTest.php new file mode 100644 index 00000000000..bcbcec96817 --- /dev/null +++ b/tests/lib/Contacts/ContactsMenu/ManagerTest.php @@ -0,0 +1,100 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Tests\Contacts\ContactsMenu; + +use OC\Contacts\ContactsMenu\ActionProviderStore; +use OC\Contacts\ContactsMenu\ContactsStore; +use OC\Contacts\ContactsMenu\Manager; +use OCP\App\IAppManager; +use OCP\Contacts\ContactsMenu\IEntry; +use OCP\Contacts\ContactsMenu\IProvider; +use PHPUnit_Framework_MockObject_MockObject; +use Test\TestCase; + +class ManagerTest extends TestCase { + + /** @var ContactsStore|PHPUnit_Framework_MockObject_MockObject */ + private $contactsStore; + + /** @var IAppManager|PHPUnit_Framework_MockObject_MockObject */ + private $appManager; + + /** @var ActionProviderStore|PHPUnit_Framework_MockObject_MockObject */ + private $actionProviderStore; + + /** @var Manager */ + private $manager; + + protected function setUp() { + parent::setUp(); + + $this->contactsStore = $this->createMock(ContactsStore::class); + $this->actionProviderStore = $this->createMock(ActionProviderStore::class); + $this->appManager = $this->createMock(IAppManager::class); + + $this->manager = new Manager($this->contactsStore, $this->actionProviderStore, $this->appManager); + } + + private function generateTestEntries() { + $entries = []; + foreach (range('Z', 'A') as $char) { + $entry = $this->createMock(IEntry::class); + $entry->expects($this->any()) + ->method('getFullName') + ->willReturn('Contact ' . $char); + $entries[] = $entry; + } + return $entries; + } + + public function testGetFilteredEntries() { + $filter = 'con'; + $user = 'user849'; + $entries = $this->generateTestEntries(); + $provider = $this->createMock(IProvider::class); + $this->contactsStore->expects($this->once()) + ->method('getContacts') + ->with($filter) + ->willReturn($entries); + $this->actionProviderStore->expects($this->once()) + ->method('getProviders') + ->willReturn([$provider]); + $provider->expects($this->exactly(25)) + ->method('process'); + $this->appManager->expects($this->once()) + ->method('isEnabledForUser') + ->with($this->equalTo('contacts'), $user) + ->willReturn(false); + $expected = [ + 'contacts' => array_slice($entries, 0, 25), + 'contactsAppEnabled' => false, + ]; + + $data = $this->manager->getEntries($user, $filter); + + $this->assertEquals($expected, $data); + } + +} diff --git a/tests/lib/Contacts/ContactsMenu/Providers/EMailproviderTest.php b/tests/lib/Contacts/ContactsMenu/Providers/EMailproviderTest.php new file mode 100644 index 00000000000..af7b21a6485 --- /dev/null +++ b/tests/lib/Contacts/ContactsMenu/Providers/EMailproviderTest.php @@ -0,0 +1,69 @@ +<?php + +/** + * @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2017 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Tests\Contacts\ContactsMenu\Providers; + +use OC\Contacts\ContactsMenu\Providers\EMailProvider; +use OCP\Contacts\ContactsMenu\IActionFactory; +use OCP\Contacts\ContactsMenu\IEntry; +use OCP\Contacts\ContactsMenu\ILinkAction; +use PHPUnit_Framework_MockObject_MockObject; +use Test\TestCase; + +class EMailproviderTest extends TestCase { + + /** @var IActionFactory|PHPUnit_Framework_MockObject_MockObject */ + private $actionFactory; + + /** @var EMailProvider */ + private $provider; + + protected function setUp() { + parent::setUp(); + + $this->actionFactory = $this->createMock(IActionFactory::class); + $this->provider = new EMailProvider($this->actionFactory); + } + + public function testProcess() { + $entry = $this->createMock(IEntry::class); + $action = $this->createMock(ILinkAction::class); + + $entry->expects($this->once()) + ->method('getEMailAddresses') + ->willReturn([ + 'user@example.com', + ]); + $this->actionFactory->expects($this->once()) + ->method('newEMailAction') + ->with($this->equalTo('icon-mail'), $this->equalTo('Mail'), $this->equalTo('user@example.com')) + ->willReturn($action); + $entry->expects($this->once()) + ->method('addAction') + ->with($action); + + $this->provider->process($entry); + } + +} |