feat(contactsmenu): Show user status

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
This commit is contained in:
Christoph Wurst 2023-10-10 16:11:25 +02:00 committed by Christopher Ng
parent b4e707059d
commit ac168cf9ff
21 changed files with 231 additions and 53 deletions

View File

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

View File

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

View File

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

View File

@ -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(),
);
}
}
}
}

View File

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

View File

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

View File

@ -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;
}
}

BIN
dist/core-main.js vendored

Binary file not shown.

BIN
dist/core-main.js.map vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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;
}

View File

@ -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
*

View File

@ -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 {

View File

@ -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);