mirror of
https://github.com/nextcloud/server.git
synced 2026-04-22 23:03:00 -04:00
Merge pull request #26939 from nextcloud/enh/app-icon-notification-bubble
Let apps toggle an unread counter on app icons
This commit is contained in:
commit
eec792446d
9 changed files with 111 additions and 23 deletions
|
|
@ -178,6 +178,7 @@ class ViewControllerTest extends TestCase {
|
|||
'icon' => '',
|
||||
'type' => 'link',
|
||||
'classes' => '',
|
||||
'unread' => 0,
|
||||
],
|
||||
'recent' => [
|
||||
'id' => 'recent',
|
||||
|
|
@ -189,6 +190,7 @@ class ViewControllerTest extends TestCase {
|
|||
'icon' => '',
|
||||
'type' => 'link',
|
||||
'classes' => '',
|
||||
'unread' => 0,
|
||||
],
|
||||
'favorites' => [
|
||||
'id' => 'favorites',
|
||||
|
|
@ -247,7 +249,8 @@ class ViewControllerTest extends TestCase {
|
|||
],
|
||||
],
|
||||
'defaultExpandedState' => false,
|
||||
'expandedState' => 'show_Quick_Access'
|
||||
'expandedState' => 'show_Quick_Access',
|
||||
'unread' => 0,
|
||||
],
|
||||
'systemtagsfilter' => [
|
||||
'id' => 'systemtagsfilter',
|
||||
|
|
@ -259,6 +262,7 @@ class ViewControllerTest extends TestCase {
|
|||
'icon' => '',
|
||||
'type' => 'link',
|
||||
'classes' => '',
|
||||
'unread' => 0,
|
||||
],
|
||||
'trashbin' => [
|
||||
'id' => 'trashbin',
|
||||
|
|
@ -270,6 +274,7 @@ class ViewControllerTest extends TestCase {
|
|||
'icon' => '',
|
||||
'type' => 'link',
|
||||
'classes' => 'pinned',
|
||||
'unread' => 0,
|
||||
],
|
||||
'shareoverview' => [
|
||||
'id' => 'shareoverview',
|
||||
|
|
@ -320,6 +325,7 @@ class ViewControllerTest extends TestCase {
|
|||
'type' => 'link',
|
||||
'expandedState' => 'show_sharing_menu',
|
||||
'defaultExpandedState' => false,
|
||||
'unread' => 0,
|
||||
]
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -612,6 +612,25 @@ nav[role='navigation'] {
|
|||
}
|
||||
}
|
||||
|
||||
.unread-counter {
|
||||
display: none;
|
||||
}
|
||||
#apps .app-icon-notification,
|
||||
#appmenu .app-icon-notification {
|
||||
fill: var(--color-error);
|
||||
}
|
||||
|
||||
#apps svg:not(.has-unread),
|
||||
#appmenu svg:not(.has-unread) {
|
||||
.app-icon-notification-mask {
|
||||
display: none;
|
||||
}
|
||||
.app-icon-notification {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Skip navigation links – show only on keyboard focus */
|
||||
.skip-navigation {
|
||||
padding: 11px;
|
||||
|
|
|
|||
2
core/js/dist/main.js
vendored
2
core/js/dist/main.js
vendored
File diff suppressed because one or more lines are too long
2
core/js/dist/main.js.map
vendored
2
core/js/dist/main.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -32,6 +32,26 @@ import OC from '../OC'
|
|||
* If the screen is bigger, the main menu is not a toggle any more.
|
||||
*/
|
||||
export const setUp = () => {
|
||||
|
||||
Object.assign(OC, {
|
||||
setNavigationCounter(id, counter) {
|
||||
const appmenuElement = document.getElementById('appmenu').querySelector('[data-id="' + id + '"] svg')
|
||||
const appsElement = document.getElementById('apps').querySelector('[data-id="' + id + '"] svg')
|
||||
if (counter === 0) {
|
||||
appmenuElement.classList.remove('has-unread')
|
||||
appsElement.classList.remove('has-unread')
|
||||
appmenuElement.getElementsByTagName('image')[0].style.mask = ''
|
||||
appsElement.getElementsByTagName('image')[0].style.mask = ''
|
||||
} else {
|
||||
appmenuElement.classList.add('has-unread')
|
||||
appsElement.classList.add('has-unread')
|
||||
appmenuElement.getElementsByTagName('image')[0].style.mask = 'url(#hole)'
|
||||
appsElement.getElementsByTagName('image')[0].style.mask = 'url(#hole)'
|
||||
}
|
||||
document.getElementById('appmenu').querySelector('[data-id="' + id + '"] .unread-counter').textContent = counter
|
||||
document.getElementById('apps').querySelector('[data-id="' + id + '"] .unread-counter').textContent = counter
|
||||
},
|
||||
})
|
||||
// init the more-apps menu
|
||||
OC.registerMenu($('#more-apps > a'), $('#navigation'))
|
||||
|
||||
|
|
|
|||
|
|
@ -57,12 +57,18 @@
|
|||
<a href="<?php print_unescaped($entry['href']); ?>"
|
||||
<?php if ($entry['active']): ?> class="active"<?php endif; ?>
|
||||
aria-label="<?php p($entry['name']); ?>">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" alt="">
|
||||
<?php if ($_['themingInvertMenu']) { ?>
|
||||
<defs><filter id="invertMenuMain-<?php p($entry['id']); ?>"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0" /></filter></defs>
|
||||
<?php } ?>
|
||||
<image x="0" y="0" width="20" height="20" preserveAspectRatio="xMinYMin meet"<?php if ($_['themingInvertMenu']) { ?> filter="url(#invertMenuMain-<?php p($entry['id']); ?>)"<?php } ?> xlink:href="<?php print_unescaped($entry['icon'] . '?v=' . $_['versionHash']); ?>" class="app-icon"></image>
|
||||
<svg width="24" height="20" viewBox="0 0 24 20" alt=""<?php if ($entry['unread'] !== 0) { ?> class="has-unread"<?php } ?>>
|
||||
<defs>
|
||||
<?php if ($_['themingInvertMenu']) { ?><filter id="invertMenuMain-<?php p($entry['id']); ?>"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0" /></filter><?php } ?>
|
||||
<mask id="hole">
|
||||
<rect width="100%" height="100%" fill="white"/>
|
||||
<circle r="4.5" cx="21" cy="3" fill="black"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<image x="2" y="0" width="20" height="20" preserveAspectRatio="xMinYMin meet"<?php if ($_['themingInvertMenu']) { ?> filter="url(#invertMenuMain-<?php p($entry['id']); ?>)"<?php } ?> xlink:href="<?php print_unescaped($entry['icon'] . '?v=' . $_['versionHash']); ?>" style="<?php if ($entry['unread'] !== 0) { ?>mask: url("#hole");<?php } ?>" class="app-icon"></image>
|
||||
<circle class="app-icon-notification" r="3" cx="21" cy="3" fill="red"/>
|
||||
</svg>
|
||||
<div class="unread-counter" aria-hidden="true"><?php p($entry['unread']); ?></div>
|
||||
<span>
|
||||
<?php p($entry['name']); ?>
|
||||
</span>
|
||||
|
|
@ -87,11 +93,19 @@
|
|||
<a href="<?php print_unescaped($entry['href']); ?>"
|
||||
<?php if ($entry['active']): ?> class="active"<?php endif; ?>
|
||||
aria-label="<?php p($entry['name']); ?>">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" alt="">
|
||||
<defs><filter id="invertMenuMore-<?php p($entry['id']); ?>"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter></defs>
|
||||
<image x="0" y="0" width="16" height="16" preserveAspectRatio="xMinYMin meet" filter="url(#invertMenuMore-<?php p($entry['id']); ?>)" xlink:href="<?php print_unescaped($entry['icon'] . '?v=' . $_['versionHash']); ?>" class="app-icon"></image>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" alt=""<?php if ($entry['unread'] !== 0) { ?> class="has-unread"<?php } ?>>
|
||||
<defs>
|
||||
<filter id="invertMenuMore-<?php p($entry['id']); ?>"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter>
|
||||
<mask id="hole">
|
||||
<rect width="100%" height="100%" fill="white"/>
|
||||
<circle r="4.5" cx="17" cy="3" fill="black"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<image x="0" y="0" width="16" height="16" preserveAspectRatio="xMinYMin meet" filter="url(#invertMenuMore-<?php p($entry['id']); ?>)" xlink:href="<?php print_unescaped($entry['icon'] . '?v=' . $_['versionHash']); ?>" style="<?php if ($entry['unread'] !== 0) { ?>mask: url("#hole");<?php } ?>" class="app-icon"></image>
|
||||
<circle class="app-icon-notification" r="3" cx="17" cy="3" fill="red"/>
|
||||
</svg>
|
||||
<span><?php p($entry['name']); ?></span>
|
||||
<div class="unread-counter" aria-hidden="true"><?php p($entry['unread']); ?></div>
|
||||
<span class="app-title"><?php p($entry['name']); ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ class NavigationManager implements INavigationManager {
|
|||
protected $entries = [];
|
||||
protected $closureEntries = [];
|
||||
protected $activeEntry;
|
||||
protected $unreadCounters = [];
|
||||
|
||||
/** @var bool */
|
||||
protected $init = false;
|
||||
/** @var IAppManager|AppManager */
|
||||
|
|
@ -97,7 +99,11 @@ class NavigationManager implements INavigationManager {
|
|||
if (!isset($entry['type'])) {
|
||||
$entry['type'] = 'link';
|
||||
}
|
||||
$this->entries[$entry['id']] = $entry;
|
||||
|
||||
$id = $entry['id'];
|
||||
$entry['unread'] = isset($this->unreadCounters[$id]) ? $this->unreadCounters[$id] : 0;
|
||||
|
||||
$this->entries[$id] = $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -319,4 +325,8 @@ class NavigationManager implements INavigationManager {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function setUnreadCounter(string $id, int $unreadCounter): void {
|
||||
$this->unreadCounters[$id] = $unreadCounter;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,4 +90,13 @@ interface INavigationManager {
|
|||
* @since 14.0.0
|
||||
*/
|
||||
public function getAll(string $type = self::TYPE_APPS): array;
|
||||
|
||||
/**
|
||||
* Set an unread counter for navigation entries
|
||||
*
|
||||
* @param string $id id of the navigation entry
|
||||
* @param int $unreadCounter Number of unread entries (0 to hide the counter which is the default)
|
||||
* @since 22.0.0
|
||||
*/
|
||||
public function setUnreadCounter(string $id, int $unreadCounter): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ class NavigationManagerTest extends TestCase {
|
|||
'icon' => 'optional',
|
||||
'href' => 'url',
|
||||
'type' => 'settings',
|
||||
'classes' => ''
|
||||
'classes' => '',
|
||||
'unread' => 0
|
||||
],
|
||||
'entry id2' => [
|
||||
'id' => 'entry id',
|
||||
|
|
@ -82,7 +83,8 @@ class NavigationManagerTest extends TestCase {
|
|||
'href' => 'url',
|
||||
'active' => false,
|
||||
'type' => 'settings',
|
||||
'classes' => ''
|
||||
'classes' => '',
|
||||
'unread' => 0
|
||||
]
|
||||
],
|
||||
[
|
||||
|
|
@ -92,7 +94,8 @@ class NavigationManagerTest extends TestCase {
|
|||
'order' => 1,
|
||||
//'icon' => 'optional',
|
||||
'href' => 'url',
|
||||
'active' => true
|
||||
'active' => true,
|
||||
'unread' => 0
|
||||
],
|
||||
'entry id2' => [
|
||||
'id' => 'entry id',
|
||||
|
|
@ -102,7 +105,8 @@ class NavigationManagerTest extends TestCase {
|
|||
'href' => 'url',
|
||||
'active' => false,
|
||||
'type' => 'link',
|
||||
'classes' => ''
|
||||
'classes' => '',
|
||||
'unread' => 0
|
||||
]
|
||||
]
|
||||
];
|
||||
|
|
@ -250,7 +254,8 @@ class NavigationManagerTest extends TestCase {
|
|||
'name' => 'Apps',
|
||||
'active' => false,
|
||||
'type' => 'settings',
|
||||
'classes' => ''
|
||||
'classes' => '',
|
||||
'unread' => 0
|
||||
]
|
||||
];
|
||||
$defaults = [
|
||||
|
|
@ -262,7 +267,8 @@ class NavigationManagerTest extends TestCase {
|
|||
'name' => 'Settings',
|
||||
'active' => false,
|
||||
'type' => 'settings',
|
||||
'classes' => ''
|
||||
'classes' => '',
|
||||
'unread' => 0
|
||||
],
|
||||
'logout' => [
|
||||
'id' => 'logout',
|
||||
|
|
@ -272,7 +278,8 @@ class NavigationManagerTest extends TestCase {
|
|||
'name' => 'Log out',
|
||||
'active' => false,
|
||||
'type' => 'settings',
|
||||
'classes' => ''
|
||||
'classes' => '',
|
||||
'unread' => 0
|
||||
]
|
||||
];
|
||||
|
||||
|
|
@ -288,7 +295,8 @@ class NavigationManagerTest extends TestCase {
|
|||
'name' => 'Test',
|
||||
'active' => false,
|
||||
'type' => 'link',
|
||||
'classes' => ''
|
||||
'classes' => '',
|
||||
'unread' => 0
|
||||
]],
|
||||
['logout' => $defaults['logout']]
|
||||
),
|
||||
|
|
@ -309,7 +317,8 @@ class NavigationManagerTest extends TestCase {
|
|||
'name' => 'Test',
|
||||
'active' => false,
|
||||
'type' => 'settings',
|
||||
'classes' => ''
|
||||
'classes' => '',
|
||||
'unread' => 0
|
||||
]],
|
||||
['logout' => $defaults['logout']]
|
||||
),
|
||||
|
|
@ -331,7 +340,8 @@ class NavigationManagerTest extends TestCase {
|
|||
'name' => 'Test',
|
||||
'active' => false,
|
||||
'type' => 'link',
|
||||
'classes' => ''
|
||||
'classes' => '',
|
||||
'unread' => 0
|
||||
]],
|
||||
['logout' => $defaults['logout']]
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue