Skip to content

Commit 9b877d4

Browse files
committed
feature #49 [Notify] Add Notify bundle (mtarld)
This PR was merged into the 2.x branch. Discussion ---------- [Notify] Add Notify bundle | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Tickets | | License | MIT In Symfony 5.3, Notifier was shipped with a Mercure bridge (symfony/symfony#39342) Then, with a simple `$this->notifier->send(new Notification('My message', ['chat/mercure']));` (and a running Mercure server), it'll be easy to create "Server-Sent-Events". Therefore, the Notify bundle idea is to listen to these events using JavaScript event sourcing and convert them as native HTML5 notifications (using the `{{ stream_notifications() }}` Twig function). (The "Notify" name was the first that came to my mind a could/should be challenged) Commits ------- e02a362 [Notify] Add Notify library
2 parents 6b629c5 + e02a362 commit 9b877d4

26 files changed

+1054
-0
lines changed

.github/workflows/test.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,23 @@ jobs:
184184
working-directory: src/LiveComponent
185185
run: php vendor/bin/simple-phpunit
186186

187+
tests-php81-high-deps:
188+
runs-on: ubuntu-latest
189+
steps:
190+
- uses: actions/checkout@master
191+
- uses: shivammathur/setup-php@v2
192+
with:
193+
php-version: '8.1'
194+
- run: php .github/build-packages.php
195+
196+
- name: Notify Dependencies
197+
uses: ramsey/composer-install@v2
198+
with:
199+
working-directory: src/Notify
200+
- name: Notify Tests
201+
working-directory: src/Notify
202+
run: php vendor/bin/simple-phpunit
203+
187204
tests-js:
188205
runs-on: ubuntu-latest
189206
steps:

src/Notify/.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/.gitattributes export-ignore
2+
/.gitignore export-ignore
3+
/phpunit.xml.dist export-ignore
4+
/Resources/assets/test export-ignore
5+
/Tests export-ignore

src/Notify/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/vendor/
2+
.phpunit.result.cache
3+
.php_cs.cache
4+
composer.lock

src/Notify/.symfony-bundle.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
branches: ["2.x"]
2+
maintained_branches: ["2.x"]
3+
doc_dir: "Resources/doc"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Notify\DependencyInjection;
13+
14+
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
15+
use Symfony\Component\Config\Definition\ConfigurationInterface;
16+
17+
/**
18+
* @author Mathias Arlaud <[email protected]>
19+
*
20+
* @internal
21+
*/
22+
final class Configuration implements ConfigurationInterface
23+
{
24+
public function getConfigTreeBuilder(): TreeBuilder
25+
{
26+
$treeBuilder = new TreeBuilder('notify');
27+
$rootNode = $treeBuilder->getRootNode();
28+
$rootNode
29+
->children()
30+
->scalarNode('mercure_hub')
31+
->info('Mercube hub service id')
32+
->defaultValue('mercure.hub.default')
33+
->end()
34+
->end()
35+
;
36+
37+
return $treeBuilder;
38+
}
39+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Notify\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Reference;
16+
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
17+
use Symfony\UX\Notify\Twig\NotifyExtension as TwigNotifyExtension;
18+
use Symfony\UX\Notify\Twig\NotifyRuntime;
19+
20+
/**
21+
* @author Mathias Arlaud <[email protected]>
22+
*
23+
* @internal
24+
*/
25+
final class NotifyExtension extends ConfigurableExtension
26+
{
27+
/**
28+
* {@inheritdoc}
29+
*/
30+
public function loadInternal(array $config, ContainerBuilder $container)
31+
{
32+
$container->register('notify.twig_extension', TwigNotifyExtension::class)
33+
->addTag('twig.extension')
34+
;
35+
36+
$container->register('notify.twig_runtime', NotifyRuntime::class)
37+
->setArguments([
38+
new Reference($config['mercure_hub']),
39+
new Reference('webpack_encore.twig_stimulus_extension'),
40+
])
41+
->addTag('twig.runtime')
42+
;
43+
}
44+
}

src/Notify/LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2020-2021 Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

src/Notify/NotifyBundle.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\Notify;
13+
14+
use Symfony\Component\HttpKernel\Bundle\Bundle;
15+
16+
/**
17+
* @author Mathias Arlaud <[email protected]>
18+
*
19+
* @final
20+
* @experimental
21+
*/
22+
class NotifyBundle extends Bundle
23+
{
24+
}

src/Notify/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Symfony UX Notify
2+
3+
Symfony UX Notify is a Symfony bundle integrating server-sent
4+
[native notifications](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API)
5+
in Symfony applications using [Mercure](https://mercure.rocks/).
6+
It is part of [the Symfony UX initiative](https://symfony.com/ux).
7+
8+
![Example of a native notification](https://github.com/symfony/ux/blob/2.x/src/Notify/Resources/doc/native-notification-example.png?raw=true)
9+
10+
**This repository is a READ-ONLY sub-tree split**. See
11+
https://github.com/symfony/ux to create issues or submit pull requests.
12+
13+
## Resources
14+
15+
- [Documentation](https://symfony.com/bundles/ux-notify/current/index.html)
16+
- [Report issues](https://github.com/symfony/ux/issues) and
17+
[send Pull Requests](https://github.com/symfony/ux/pulls)
18+
in the [main Symfony UX repository](https://github.com/symfony/ux)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
3+
class default_1 extends Controller {
4+
constructor() {
5+
super(...arguments);
6+
this.eventSources = [];
7+
}
8+
initialize() {
9+
const errorMessages = [];
10+
if (!this.hasHubValue)
11+
errorMessages.push('A "hub" value pointing to the Mercure hub must be provided.');
12+
if (!this.hasTopicsValue)
13+
errorMessages.push('A "topics" value must be provided.');
14+
if (errorMessages.length)
15+
throw new Error(errorMessages.join(' '));
16+
this.eventSources = this.topicsValue.map((topic) => {
17+
const u = new URL(this.hubValue);
18+
u.searchParams.append('topic', topic);
19+
return new EventSource(u);
20+
});
21+
}
22+
connect() {
23+
if (!('Notification' in window)) {
24+
console.warn('This browser does not support desktop notifications.');
25+
return;
26+
}
27+
this.eventSources.forEach((eventSource) => {
28+
eventSource.addEventListener('message', (event) => this._notify(JSON.parse(event.data).summary));
29+
});
30+
this._dispatchEvent('notify:connect', { eventSources: this.eventSources });
31+
}
32+
disconnect() {
33+
this.eventSources.forEach((eventSource) => {
34+
eventSource.removeEventListener('message', this._notify);
35+
eventSource.close();
36+
});
37+
this.eventSources = [];
38+
}
39+
_notify(content) {
40+
if (!content)
41+
return;
42+
if ('granted' === Notification.permission) {
43+
new Notification(content);
44+
return;
45+
}
46+
if ('denied' !== Notification.permission) {
47+
Notification.requestPermission().then((permission) => {
48+
if ('granted' === permission) {
49+
new Notification(content);
50+
}
51+
});
52+
}
53+
}
54+
_dispatchEvent(name, payload) {
55+
this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true }));
56+
}
57+
}
58+
default_1.values = {
59+
hub: String,
60+
topics: Array,
61+
};
62+
63+
export { default_1 as default };
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('../../../../jest.config.js');
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "@symfony/ux-notify",
3+
"description": "Native notification integration for Symfony using Mercure",
4+
"license": "MIT",
5+
"version": "1.0.0",
6+
"symfony": {
7+
"controllers": {
8+
"notify": {
9+
"main": "dist/controller.js",
10+
"webpackMode": "eager",
11+
"fetch": "eager",
12+
"enabled": true
13+
}
14+
}
15+
},
16+
"peerDependencies": {
17+
"@hotwired/stimulus": "^3.0.0"
18+
},
19+
"devDependencies": {
20+
"@hotwired/stimulus": "^3.0.0"
21+
}
22+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
12+
import { Controller } from '@hotwired/stimulus';
13+
14+
/**
15+
* @author Mathias Arlaud <[email protected]>
16+
*/
17+
export default class extends Controller {
18+
static values = {
19+
hub: String,
20+
topics: Array,
21+
};
22+
23+
eventSources: Array<EventSource> = [];
24+
25+
initialize() {
26+
const errorMessages: Array<string> = [];
27+
28+
if (!this.hasHubValue) errorMessages.push('A "hub" value pointing to the Mercure hub must be provided.');
29+
if (!this.hasTopicsValue) errorMessages.push('A "topics" value must be provided.');
30+
31+
if (errorMessages.length) throw new Error(errorMessages.join(' '));
32+
33+
this.eventSources = this.topicsValue.map((topic) => {
34+
const u = new URL(this.hubValue);
35+
u.searchParams.append('topic', topic);
36+
37+
return new EventSource(u);
38+
});
39+
}
40+
41+
connect() {
42+
if (!('Notification' in window)) {
43+
console.warn('This browser does not support desktop notifications.');
44+
45+
return;
46+
}
47+
48+
this.eventSources.forEach((eventSource) => {
49+
eventSource.addEventListener('message', (event) => this._notify(JSON.parse(event.data).summary));
50+
});
51+
52+
this._dispatchEvent('notify:connect', { eventSources: this.eventSources });
53+
}
54+
55+
disconnect() {
56+
this.eventSources.forEach((eventSource) => {
57+
eventSource.removeEventListener('message', this._notify);
58+
eventSource.close();
59+
});
60+
61+
this.eventSources = [];
62+
}
63+
64+
_notify(content: string | undefined) {
65+
if (!content) return;
66+
67+
if ('granted' === Notification.permission) {
68+
new Notification(content);
69+
70+
return;
71+
}
72+
73+
if ('denied' !== Notification.permission) {
74+
Notification.requestPermission().then((permission) => {
75+
if ('granted' === permission) {
76+
new Notification(content);
77+
}
78+
});
79+
}
80+
}
81+
82+
_dispatchEvent(name: string, payload: any) {
83+
this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true }));
84+
}
85+
}

0 commit comments

Comments
 (0)