diff --git a/apps/user_status/appinfo/info.xml b/apps/user_status/appinfo/info.xml index 6808c07ec7f87..a574b1ed4a1a6 100644 --- a/apps/user_status/appinfo/info.xml +++ b/apps/user_status/appinfo/info.xml @@ -25,4 +25,7 @@ OCA\UserStatus\BackgroundJob\ClearOldStatusesBackgroundJob + + OCA\UserStatus\ContactsMenu\StatusProvider + diff --git a/apps/user_status/composer/composer/autoload_classmap.php b/apps/user_status/composer/composer/autoload_classmap.php index a0e3572a87bcb..ecdd83dbb6890 100644 --- a/apps/user_status/composer/composer/autoload_classmap.php +++ b/apps/user_status/composer/composer/autoload_classmap.php @@ -12,6 +12,7 @@ 'OCA\\UserStatus\\Capabilities' => $baseDir . '/../lib/Capabilities.php', 'OCA\\UserStatus\\Connector\\UserStatus' => $baseDir . '/../lib/Connector/UserStatus.php', 'OCA\\UserStatus\\Connector\\UserStatusProvider' => $baseDir . '/../lib/Connector/UserStatusProvider.php', + 'OCA\\UserStatus\\ContactsMenu\\StatusProvider' => $baseDir . '/../lib/ContactsMenu/StatusProvider.php', 'OCA\\UserStatus\\Controller\\HeartbeatController' => $baseDir . '/../lib/Controller/HeartbeatController.php', 'OCA\\UserStatus\\Controller\\PredefinedStatusController' => $baseDir . '/../lib/Controller/PredefinedStatusController.php', 'OCA\\UserStatus\\Controller\\StatusesController' => $baseDir . '/../lib/Controller/StatusesController.php', diff --git a/apps/user_status/composer/composer/autoload_static.php b/apps/user_status/composer/composer/autoload_static.php index 58e77db6721b6..55be0c04d430d 100644 --- a/apps/user_status/composer/composer/autoload_static.php +++ b/apps/user_status/composer/composer/autoload_static.php @@ -27,6 +27,7 @@ class ComposerStaticInitUserStatus 'OCA\\UserStatus\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php', 'OCA\\UserStatus\\Connector\\UserStatus' => __DIR__ . '/..' . '/../lib/Connector/UserStatus.php', 'OCA\\UserStatus\\Connector\\UserStatusProvider' => __DIR__ . '/..' . '/../lib/Connector/UserStatusProvider.php', + 'OCA\\UserStatus\\ContactsMenu\\StatusProvider' => __DIR__ . '/..' . '/../lib/ContactsMenu/StatusProvider.php', 'OCA\\UserStatus\\Controller\\HeartbeatController' => __DIR__ . '/..' . '/../lib/Controller/HeartbeatController.php', 'OCA\\UserStatus\\Controller\\PredefinedStatusController' => __DIR__ . '/..' . '/../lib/Controller/PredefinedStatusController.php', 'OCA\\UserStatus\\Controller\\StatusesController' => __DIR__ . '/..' . '/../lib/Controller/StatusesController.php', diff --git a/apps/user_status/lib/ContactsMenu/StatusProvider.php b/apps/user_status/lib/ContactsMenu/StatusProvider.php new file mode 100644 index 0000000000000..3c37d6b080ff4 --- /dev/null +++ b/apps/user_status/lib/ContactsMenu/StatusProvider.php @@ -0,0 +1,65 @@ + + * + * @author 2023 Christoph Wurst + * + * @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 . + */ + +namespace OCA\UserStatus\ContactsMenu; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Service\StatusService; +use OCP\Contacts\ContactsMenu\IBulkProvider; +use OCP\Contacts\ContactsMenu\IEntry; +use function array_combine; +use function array_filter; +use function array_map; + +class StatusProvider implements IBulkProvider { + + public function __construct(private StatusService $statusService) { + } + + public function process(array $entries): void { + $uids = array_filter( + array_map(fn (IEntry $entry): ?string => $entry->getProperty('UID'), $entries) + ); + + $statuses = $this->statusService->findByUserIds($uids); + $indexed = array_combine( + array_map(fn(UserStatus $status) => $status->getUserId(), $statuses), + $statuses + ); + + foreach ($entries as $entry) { + $uid = $entry->getProperty('UID'); + if ($uid !== null && isset($indexed[$uid])) { + $status = $indexed[$uid]; + $entry->setStatus( + $status->getStatus(), + $status->getCustomMessage(), + $status->getCustomIcon(), + ); + } + } + } + +} diff --git a/core/src/components/ContactsMenu/Contact.vue b/core/src/components/ContactsMenu/Contact.vue index 84869814c2fa0..547838d7deb5c 100644 --- a/core/src/components/ContactsMenu/Contact.vue +++ b/core/src/components/ContactsMenu/Contact.vue @@ -25,27 +25,37 @@ :href="contact.profileUrl" class="contact__avatar-wrapper"> + :url="contact.avatar" + :preloaded-user-status="preloadedUserStatus" /> + :size="44" + :user="contact.isUser ? contact.uid : undefined" + :is-no-user="!contact.isUser" + :display-name="contact.avatarLabel" + :preloaded-user-status="preloadedUserStatus" /> + :url="contact.avatar" + :preloaded-user-status="preloadedUserStatus" />
{{ contact.fullName }}
{{ contact.lastMessage }}
- +
{{ contact.statusMessage }}
+
@@ -97,6 +107,16 @@ export default { } return this.contact.actions }, + preloadedUserStatus() { + if (this.contact.status) { + return { + status: this.contact.status, + message: this.contact.statusMessage, + icon: this.contact.statusIcon, + } + } + return undefined + } }, } @@ -118,18 +138,15 @@ export default { } &__avatar-wrapper { - height: 32px; } &__avatar { - height: 32px; - width: 32px; display: inherit; } &__body { flex-grow: 1; - padding-left: 8px; + padding-left: 10px; min-width: 0; div { @@ -137,9 +154,16 @@ export default { width: 100%; overflow-x: hidden; text-overflow: ellipsis; + margin: -1px 0; + } + div:first-of-type { + margin-top: 0; + } + div:last-of-type { + margin-bottom: 0; } - .last-message, .email-address { + &__last-message, &__status-message, &__email-address { color: var(--color-text-maxcontrast); } } diff --git a/core/src/tests/views/ContactsMenu.spec.js b/core/src/tests/views/ContactsMenu.spec.js index 66c0532322bf1..6b438a4998e46 100644 --- a/core/src/tests/views/ContactsMenu.spec.js +++ b/core/src/tests/views/ContactsMenu.spec.js @@ -139,7 +139,7 @@ describe('ContactsMenu', function() { emailAddresses: [], } ], - contactsAppEnabled: false, + contactsAppEnabled: true, }, }) @@ -149,26 +149,6 @@ describe('ContactsMenu', function() { expect(view.vm.contacts.length).toBe(2) expect(view.text()).toContain('Acosta Lancaster') expect(view.text()).toContain('Adeline Snider') - }) - - it('shows link ot Contacts', async () => { - const view = shallowMount(ContactsMenu) - axios.post.mockResolvedValue({ - data: { - contacts: [ - { - id: 1, - }, - { - id: 2, - }, - ], - contactsAppEnabled: true, - }, - }) - - await view.vm.handleOpen() - - expect(view.text()).toContain('Show all contacts …') + expect(view.text()).toContain('Show all contacts') }) }) diff --git a/core/src/views/ContactsMenu.vue b/core/src/views/ContactsMenu.vue index fbb040eb86001..e3f337d77dcc0 100644 --- a/core/src/views/ContactsMenu.vue +++ b/core/src/views/ContactsMenu.vue @@ -58,10 +58,10 @@ @@ -75,6 +75,7 @@ import debounce from 'debounce' import { getCurrentUser } from '@nextcloud/auth' import { generateUrl } from '@nextcloud/router' import Magnify from 'vue-material-design-icons/Magnify.vue' +import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' import NcHeaderMenu from '@nextcloud/vue/dist/Components/NcHeaderMenu.js' import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' @@ -91,6 +92,7 @@ export default { Contact, Contacts, Magnify, + NcButton, NcEmptyContent, NcHeaderMenu, NcLoadingIcon, @@ -178,14 +180,9 @@ export default { overflow-y: auto; &__footer { - text-align: center; - - a { - display: block; - width: 100%; - padding: 12px 0; - opacity: .5; - } + display: flex; + flex-direction: column; + align-items: center; } } diff --git a/dist/core-main.js b/dist/core-main.js index bcc6b70108a54..e42daec850751 100644 --- a/dist/core-main.js +++ b/dist/core-main.js @@ -1,3 +1,3 @@ /*! For license information please see core-main.js.LICENSE.txt */ -!function(){var e,i,o,r={74589:function(e,i,o){"use strict";var r={};o.r(r),o.d(r,{deleteKey:function(){return k},getApps:function(){return C},getKeys:function(){return x},getValue:function(){return y},setValue:function(){return w}});var s={};o.r(s),o.d(s,{formatLinksPlain:function(){return Un},formatLinksRich:function(){return Ln},plainToRich:function(){return Hn},richToPlain:function(){return jn}});var a={};o.r(a),o.d(a,{dismiss:function(){return Yn},query:function(){return Wn}}),o(28594),o(35666);var c=o(69183),u=o(59050),l=o(19755),h=o.n(l),d=o(64024),p=o(25108),A={updatableNotification:null,getDefaultNotificationFunction:null,setDefault:function(t){this.getDefaultNotificationFunction=t},hide:function(t,e){u.default.isFunction(t)&&(e=t,t=void 0),t?(t.each((function(){h()(this)[0].toastify?h()(this)[0].toastify.hideToast():p.error("cannot hide toast because object is not set"),this===this.updatableNotification&&(this.updatableNotification=null)})),e&&e.call(),this.getDefaultNotificationFunction&&this.getDefaultNotificationFunction()):p.error("Missing argument $row in OC.Notification.hide() call, caller needs to be adjusted to only dismiss its own notification")},showHtml:function(t,e){(e=e||{}).isHTML=!0,e.timeout=e.timeout?e.timeout:d.Rl;var n=(0,d.PV)(t,e);return n.toastElement.toastify=n,h()(n.toastElement)},show:function(t,e){(e=e||{}).timeout=e.timeout?e.timeout:d.Rl;var n=(0,d.PV)(function(t){return t.toString().split("&").join("&").split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'")}(t),e);return n.toastElement.toastify=n,h()(n.toastElement)},showUpdate:function(t){return this.updatableNotification&&this.updatableNotification.hideToast(),this.updatableNotification=(0,d.PV)(t,{timeout:d.Rl}),this.updatableNotification.toastElement.toastify=this.updatableNotification,h()(this.updatableNotification.toastElement)},showTemporary:function(t,e){(e=e||{}).timeout=e.timeout||d.TN;var n=(0,d.PV)(t,e);return n.toastElement.toastify=n,h()(n.toastElement)},isHidden:function(){return!h()("#content").find(".toastify").length}},f=u.default.throttle((function(){A.showTemporary(t("core","Connection to server lost"))}),7e3,{trailing:!1}),g=!1,m={enableDynamicSlideToggle:function(){g=!0},showAppSidebar:function(t){(t||h()("#app-sidebar")).removeClass("disappear").show(),h()("#app-content").trigger(new(h().Event)("appresized"))},hideAppSidebar:function(t){(t||h()("#app-sidebar")).hide().addClass("disappear"),h()("#app-content").trigger(new(h().Event)("appresized"))}},v=o(79753);function b(t,e,n){"post"!==t&&"delete"!==t||!It.PasswordConfirmation.requiresPasswordConfirmation()?(n=n||{},h().ajax({type:t.toUpperCase(),url:(0,v.generateOcsUrl)("apps/provisioning_api/api/v1/config/apps")+e,data:n.data||{},success:n.success,error:n.error})):It.PasswordConfirmation.requirePasswordConfirmation(_.bind(b,this,t,e,n))}function C(t){b("get","",t)}function x(t,e){b("get","/"+t,e)}function y(t,e,n,i){(i=i||{}).data={defaultValue:n},b("get","/"+t+"/"+e,i)}function w(t,e,n,i){(i=i||{}).data={value:n},b("post","/"+t+"/"+e,i)}function k(t,e,n){b("delete","/"+t+"/"+e,n)}var B=window.oc_appconfig||{},E={getValue:function(t,e,n,i){y(t,e,n,{success:i})},setValue:function(t,e,n){w(t,e,n)},getApps:function(t){C({success:t})},getKeys:function(t,e){x(t,{success:e})},deleteKey:function(t,e){k(t,e)}},D=void 0!==window._oc_appswebroots&&window._oc_appswebroots,I=o(72316),S=o.n(I),T=o(76591),M=o(25108),P={create:"POST",update:"PROPPATCH",patch:"PROPPATCH",delete:"DELETE",read:"PROPFIND"};function O(t,e){if(u.default.isArray(t))return u.default.map(t,(function(t){return O(t,e)}));var n={href:t.href};return u.default.each(t.propStat,(function(t){if("HTTP/1.1 200 OK"===t.status)for(var i in t.properties){var o=i;i in e&&(o=e[i]),n[o]=t.properties[i]}})),n.id||(n.id=z(n.href)),n}function z(t){var e=t.indexOf("?");e>0&&(t=t.substr(0,e));var n,i=t.split("/");do{n=i[i.length-1],i.pop()}while(!n&&i.length>0);return n}function R(t){return t>=200&&t<=299}function N(t,e,n,i){return t.propPatch(e.url,function(t,e){var n,i={};for(n in t){var o=e[n],r=t[n];o||(M.warn('No matching DAV property for property "'+n),o=n),(u.default.isBoolean(r)||u.default.isNumber(r))&&(r=""+r),i[o]=r}return i}(n.changed,e.davProperties),i).then((function(t){R(t.status)?u.default.isFunction(e.success)&&e.success(n.toJSON()):u.default.isFunction(e.error)&&e.error(t)}))}var H=S().noConflict();Object.assign(H,{davCall:function(t,e){var n=new T.dav.Client({baseUrl:t.url,xmlNamespaces:u.default.extend({"DAV:":"d","http://owncloud.org/ns":"oc"},t.xmlNamespaces||{})});n.resolveUrl=function(){return t.url};var i=u.default.extend({"X-Requested-With":"XMLHttpRequest",requesttoken:OC.requestToken},t.headers);return"PROPFIND"===t.type?function(t,e,n,i){return t.propFind(e.url,u.default.values(e.davProperties)||[],e.depth,i).then((function(t){if(R(t.status)){if(u.default.isFunction(e.success)){var n=u.default.invert(e.davProperties),i=O(t.body,n);e.depth>0&&i.shift(),e.success(i)}}else u.default.isFunction(e.error)&&e.error(t)}))}(n,t,0,i):"PROPPATCH"===t.type?N(n,t,e,i):"MKCOL"===t.type?function(t,e,n,i){return t.request(e.type,e.url,i,null).then((function(o){R(o.status)?N(t,e,n,i):u.default.isFunction(e.error)&&e.error(o)}))}(n,t,e,i):function(t,e,n,i){return i["Content-Type"]="application/json",t.request(e.type,e.url,i,e.data).then((function(t){if(R(t.status)){if(u.default.isFunction(e.success)){if("PUT"===e.type||"POST"===e.type||"MKCOL"===e.type){var i=t.body||n.toJSON(),o=t.xhr.getResponseHeader("Content-Location");return"POST"===e.type&&o&&(i.id=z(o)),void e.success(i)}if(207===t.status){var r=u.default.invert(e.davProperties);e.success(O(t.body,r))}else e.success(t.body)}}else u.default.isFunction(e.error)&&e.error(t)}))}(n,t,e,i)},davSync:function(t){return function(e,n,i){var o={type:P[e]||e},r=n instanceof t.Collection;if("update"===e&&(n.hasInnerCollection?o.type="MKCOL":(n.usePUT||n.collection&&n.collection.usePUT)&&(o.type="PUT")),i.url||(o.url=u.default.result(n,"url")||function(){throw new Error('A "url" property or function must be specified')}()),null!=i.data||!n||"create"!==e&&"update"!==e&&"patch"!==e||(o.data=JSON.stringify(i.attrs||n.toJSON(i))),"PROPFIND"!==o.type&&(o.processData=!1),"PROPFIND"===o.type||"PROPPATCH"===o.type){var s=n.davProperties;!s&&n.model&&(s=n.model.prototype.davProperties),s&&(u.default.isFunction(s)?o.davProperties=s.call(n):o.davProperties=s),o.davProperties=u.default.extend(o.davProperties||{},i.davProperties),u.default.isUndefined(i.depth)&&(i.depth=r?1:0)}var a=i.error;i.error=function(t,e,n){i.textStatus=e,i.errorThrown=n,a&&a.call(i.context,t,e,n)};var c=i.xhr=t.davCall(u.default.extend(o,i),n);return n.trigger("request",n,c,i),c}}(H)});var j=H,L=o(65358),U=window._oc_config||{},F=document.getElementsByTagName("head")[0].getAttribute("data-user"),W=document.getElementsByTagName("head")[0].getAttribute("data-user-displayname"),Y=void 0!==F&&F,Q=o(96384),q=o(59546),G=o(62520);function X(t){return X="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},X(t)}var V={YES_NO_BUTTONS:70,OK_BUTTONS:71,FILEPICKER_TYPE_CHOOSE:1,FILEPICKER_TYPE_MOVE:2,FILEPICKER_TYPE_COPY:3,FILEPICKER_TYPE_COPY_MOVE:4,FILEPICKER_TYPE_CUSTOM:5,dialogsCounter:0,alert:function(t,e,n,i){this.message(t,e,"alert",V.OK_BUTTON,n,i)},info:function(t,e,n,i){this.message(t,e,"info",V.OK_BUTTON,n,i)},confirm:function(t,e,n,i){return this.message(t,e,"notice",V.YES_NO_BUTTONS,n,i)},confirmDestructive:function(t,e,n,i,o){return this.message(t,e,"none",n,i,void 0===o||o)},confirmHtml:function(t,e,n,i){return this.message(t,e,"notice",V.YES_NO_BUTTONS,n,i,!0)},prompt:function(e,n,i,o,r,s){return h().when(this._getMessageTemplate()).then((function(a){var c="oc-dialog-"+V.dialogsCounter+"-content",l="#"+c,d=a.octemplate({dialog_name:c,title:n,message:e,type:"notice"}),p=h()("");p.attr("type",s?"password":"text").attr("id",c+"-input").attr("placeholder",r);var A=h()("