feat(contactsmenu): Show user status

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
pull/40852/head
Christoph Wurst 7 months ago committed by Christopher Ng
parent b4e707059d
commit ac168cf9ff

@ -25,4 +25,7 @@
<background-jobs>
<job>OCA\UserStatus\BackgroundJob\ClearOldStatusesBackgroundJob</job>
</background-jobs>
<contactsmenu>
<provider>OCA\UserStatus\ContactsMenu\StatusProvider</provider>
</contactsmenu>
</info>

@ -12,6 +12,7 @@ return array(
'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',

@ -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',

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/*
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2023 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 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(),
);
}
}
}
}

@ -25,27 +25,37 @@
:href="contact.profileUrl"
class="contact__avatar-wrapper">
<NcAvatar class="contact__avatar"
:is-no-user="true"
:size="44"
:user="contact.isUser ? contact.uid : undefined"
:is-no-user="!contact.isUser"
:display-name="contact.avatarLabel"
:url="contact.avatar" />
:url="contact.avatar"
:preloaded-user-status="preloadedUserStatus" />
</a>
<a v-else-if="contact.profileUrl"
:href="contact.profileUrl">
<NcAvatar class="contact__avatar"
:is-no-user="true"
:display-name="contact.avatarLabel" />
:size="44"
:user="contact.isUser ? contact.uid : undefined"
:is-no-user="!contact.isUser"
:display-name="contact.avatarLabel"
:preloaded-user-status="preloadedUserStatus" />
</a>
<NcAvatar v-else
:size="44"
class="contact__avatar"
:is-no-user="true"
:user="contact.isUser ? contact.uid : undefined"
:is-no-user="!contact.isUser"
:display-name="contact.avatarLabel"
:url="contact.avatar" />
:url="contact.avatar"
:preloaded-user-status="preloadedUserStatus" />
<a class="contact__body"
:href="contact.profileUrl || contact.topAction?.hyperlink">
<div class="contact__body__full-name">{{ contact.fullName }}</div>
<div v-if="contact.lastMessage" class="contact__body__last-message">{{ contact.lastMessage }}</div>
<div class="contact__body__email-address">{{ contact.emailAddresses[0] }}</div>
<div v-if="contact.statusMessage" class="contact__body__status-message">{{ contact.statusMessage }}</div>
<div v-else class="contact__body__email-address">{{ contact.emailAddresses[0] }}</div>
</a>
<NcActions v-if="actions.length"
:inline="contact.topAction ? 1 : 0">
@ -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
}
},
}
</script>
@ -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);
}
}

@ -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')
})
})

@ -58,10 +58,10 @@
</ul>
</div>
<div v-if="contactsAppEnabled" class="contactsmenu__menu__content__footer">
<a :href="contactsAppURL">{{ t('core', 'Show all contacts') }}</a>
<NcButton type="tertiary" :href="contactsAppURL">{{ t('core', 'Show all contacts') }}</NcButton>
</div>
<div v-else-if="canInstallApp" class="contactsmenu__menu__content__footer">
<a :href="contactsAppMgmtURL">{{ t('core', 'Install the Contacts app') }}</a>
<NcButton type="tertiary" :href="contactsAppMgmtURL">{{ t('core', 'Install the Contacts app') }}</NcButton>
</div>
</div>
</div>
@ -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;
}
}

4
dist/core-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -210,6 +210,7 @@ return array(
'OCP\\Constants' => $baseDir . '/lib/public/Constants.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\\IBulkProvider' => $baseDir . '/lib/public/Contacts/ContactsMenu/IBulkProvider.php',
'OCP\\Contacts\\ContactsMenu\\IContactsStore' => $baseDir . '/lib/public/Contacts/ContactsMenu/IContactsStore.php',
'OCP\\Contacts\\ContactsMenu\\IEntry' => $baseDir . '/lib/public/Contacts/ContactsMenu/IEntry.php',
'OCP\\Contacts\\ContactsMenu\\ILinkAction' => $baseDir . '/lib/public/Contacts/ContactsMenu/ILinkAction.php',

@ -243,6 +243,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Constants' => __DIR__ . '/../../..' . '/lib/public/Constants.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\\IBulkProvider' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IBulkProvider.php',
'OCP\\Contacts\\ContactsMenu\\IContactsStore' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IContactsStore.php',
'OCP\\Contacts\\ContactsMenu\\IEntry' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IEntry.php',
'OCP\\Contacts\\ContactsMenu\\ILinkAction' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/ILinkAction.php',

@ -33,6 +33,7 @@ use OC\Contacts\ContactsMenu\Providers\EMailProvider;
use OC\Contacts\ContactsMenu\Providers\LocalTimeProvider;
use OC\Contacts\ContactsMenu\Providers\ProfileProvider;
use OCP\AppFramework\QueryException;
use OCP\Contacts\ContactsMenu\IBulkProvider;
use OCP\Contacts\ContactsMenu\IProvider;
use OCP\IServerContainer;
use OCP\IUser;
@ -47,18 +48,26 @@ class ActionProviderStore {
}
/**
* @return IProvider[]
* @return list<IProvider|IBulkProvider>
* @throws Exception
*/
public function getProviders(IUser $user): array {
$appClasses = $this->getAppProviderClasses($user);
$providerClasses = $this->getServerProviderClasses();
$allClasses = array_merge($providerClasses, $appClasses);
/** @var list<IProvider|IBulkProvider> $providers */
$providers = [];
foreach ($allClasses as $class) {
try {
$providers[] = $this->serverContainer->get($class);
$provider = $this->serverContainer->get($class);
if ($provider instanceof IProvider || $provider instanceof IBulkProvider) {
$providers[] = $provider;
} else {
$this->logger->warning('Ignoring invalid contacts menu provider', [
'class' => $class,
]);
}
} catch (QueryException $ex) {
$this->logger->error(
'Could not load contacts menu action provider ' . $class,

@ -268,8 +268,10 @@ class ContactsStore implements IContactsStore {
if (isset($contact['UID'])) {
$uid = $contact['UID'];
$entry->setId($uid);
$entry->setProperty('isUser', false);
if (isset($contact['isLocalSystemBook'])) {
$avatar = $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $uid, 'size' => 64]);
$entry->setProperty('isUser', true);
} elseif (isset($contact['FN'])) {
$avatar = $this->urlGenerator->linkToRouteAbsolute('core.GuestAvatar.getAvatar', ['guestName' => $contact['FN'], 'size' => 64]);
} else {

@ -29,6 +29,7 @@ namespace OC\Contacts\ContactsMenu;
use OCP\Contacts\ContactsMenu\IAction;
use OCP\Contacts\ContactsMenu\IEntry;
use function array_merge;
class Entry implements IEntry {
/** @var string|int|null */
@ -50,6 +51,10 @@ class Entry implements IEntry {
private array $properties = [];
private ?string $status = null;
private ?string $statusMessage = null;
private ?string $statusIcon = null;
public function setId(string $id): void {
$this->id = $id;
}
@ -102,6 +107,14 @@ class Entry implements IEntry {
$this->sortActions();
}
public function setStatus(string $status,
string $statusMessage = null,
string $icon = null): void {
$this->status = $status;
$this->statusMessage = $statusMessage;
$this->statusIcon = $icon;
}
/**
* @return IAction[]
*/
@ -127,11 +140,15 @@ class Entry implements IEntry {
});
}
public function setProperty(string $propertyName, mixed $value) {
$this->properties[$propertyName] = $value;
}
/**
* @param array $contact key-value array containing additional properties
* @param array $properties key-value array containing additional properties
*/
public function setProperties(array $contact): void {
$this->properties = $contact;
public function setProperties(array $properties): void {
$this->properties = array_merge($this->properties, $properties);
}
public function getProperty(string $key): mixed {
@ -142,7 +159,7 @@ class Entry implements IEntry {
}
/**
* @return array{id: int|string|null, fullName: string, avatar: string|null, topAction: mixed, actions: array, lastMessage: '', emailAddresses: string[], profileTitle: string|null, profileUrl: string|null}
* @return array{id: int|string|null, fullName: string, avatar: string|null, topAction: mixed, actions: array, lastMessage: '', emailAddresses: string[], profileTitle: string|null, profileUrl: string|null, status: string|null, statusMessage: null|string, statusIcon: null|string, isUser: bool, uid: mixed}
*/
public function jsonSerialize(): array {
$topAction = !empty($this->actions) ? $this->actions[0]->jsonSerialize() : null;
@ -160,6 +177,11 @@ class Entry implements IEntry {
'emailAddresses' => $this->getEMailAddresses(),
'profileTitle' => $this->profileTitle,
'profileUrl' => $this->profileUrl,
'status' => $this->status,
'statusMessage' => $this->statusMessage,
'statusIcon' => $this->statusIcon,
'isUser' => $this->getProperty('isUser') === true,
'uid' => $this->getProperty('UID'),
];
}
}

@ -28,7 +28,9 @@ namespace OC\Contacts\ContactsMenu;
use Exception;
use OCP\App\IAppManager;
use OCP\Constants;
use OCP\Contacts\ContactsMenu\IBulkProvider;
use OCP\Contacts\ContactsMenu\IEntry;
use OCP\Contacts\ContactsMenu\IProvider;
use OCP\IConfig;
use OCP\IUser;
@ -92,9 +94,14 @@ class Manager {
*/
private function processEntries(array $entries, IUser $user): void {
$providers = $this->actionProviderStore->getProviders($user);
foreach ($entries as $entry) {
foreach ($providers as $provider) {
$provider->process($entry);
foreach ($providers as $provider) {
if ($provider instanceof IBulkProvider && !($provider instanceof IProvider)) {
$provider->process($entries);
} elseif ($provider instanceof IProvider && !($provider instanceof IBulkProvider)) {
foreach ($entries as $entry) {
$provider->process($entry);
}
}
}
}

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 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;
/**
* Process contacts menu entries in bulk
*
* @since 28.0
*/
interface IBulkProvider {
/**
* @since 28.0
* @param list<IEntry> $entries
* @return void
*/
public function process(array $entries): void;
}

@ -53,6 +53,19 @@ interface IEntry extends JsonSerializable {
*/
public function addAction(IAction $action): void;
/**
* Set the (system) contact's user status
*
* @since 28.0
* @param string $status
* @param string $statusMessage
* @param string|null $icon
* @return void
*/
public function setStatus(string $status,
string $statusMessage = null,
string $icon = null): void;
/**
* Get an arbitrary property from the contact
*

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/**
* @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
@ -23,6 +26,10 @@
namespace OCP\Contacts\ContactsMenu;
/**
* Process contacts menu entries
*
* @see IBulkProvider for providers that work with the full dataset at once
*
* @since 12.0
*/
interface IProvider {

@ -103,6 +103,11 @@ class EntryTest extends TestCase {
'emailAddresses' => ['user@example.com'],
'profileTitle' => null,
'profileUrl' => null,
'status' => null,
'statusMessage' => null,
'statusIcon' => null,
'isUser' => false,
'uid' => null,
];
$this->entry->setId(123);

Loading…
Cancel
Save