├── .gitignore ├── .php_cs ├── Command └── ServerCommand.php ├── Context ├── NotificationContext.php ├── NotificationContextInterface.php ├── NullContext.php └── PusherIdentity.php ├── DependencyInjection ├── CompilerPass │ └── PusherCompilerPass.php ├── Configuration.php └── GosNotificationExtension.php ├── Event ├── NotificationEvents.php ├── NotificationPublishedEvent.php └── NotificationPushedEvent.php ├── Exception ├── NotFoundNotificationException.php ├── NotificationException.php └── NotificationServerException.php ├── Fetcher ├── FetcherInterface.php └── RedisFetcher.php ├── GosNotificationBundle.php ├── LICENSE ├── Listener └── NotificationListener.php ├── Model ├── Message │ ├── Message.php │ ├── MessageInterface.php │ └── PatternMessage.php ├── Notification.php └── NotificationInterface.php ├── NotificationCenter.php ├── NotificationManipulatorInterface.php ├── Procedure └── NotificationProcedure.php ├── Processor └── ProcessorInterface.php ├── Publisher ├── PublisherInterface.php └── RedisPublisher.php ├── Pusher ├── AbstractPusher.php ├── ProcessorDelegate.php ├── ProcessorTrait.php ├── PusherInterface.php ├── PusherLoopAwareInterface.php ├── PusherRegistry.php ├── RedisPusher.php └── WebsocketPusher.php ├── README.md ├── Redis └── IndexOfElement.php ├── Resources ├── config │ ├── pubsub │ │ ├── redis │ │ │ └── notification.yml │ │ └── websocket │ │ │ ├── notification.yml │ │ │ └── notification_rpc.yml │ └── services │ │ └── services.yml └── public │ └── js │ └── notification │ ├── .gitignore │ ├── README.md │ ├── dist │ ├── gos-notification.min.js │ ├── gos-notification.min.js.map │ └── notification-widget.css │ ├── external │ └── ng-scrollbar-fix.js │ ├── gulpfile.js │ ├── package.json │ └── src │ ├── controller │ ├── BoardController.coffee │ ├── RealtimeController.coffee │ └── ToggleController.coffee │ ├── directive │ └── GosNotification.coffee │ ├── gos-notification.coffee │ ├── service │ ├── BoardService.coffee │ ├── NotificationService.coffee │ └── WebsocketService.coffee │ ├── style │ └── notification-widget.less │ └── views │ └── notification-list.html ├── Router └── Dumper │ └── RedisDumper.php ├── Serializer ├── NotificationContextSerializer.php ├── NotificationContextSerializerInterface.php ├── NotificationSerializer.php └── NotificationSerializerInterface.php ├── Server ├── PubSubServer.php ├── ServerNotificationProcessor.php └── ServerNotificationProcessorInterface.php ├── Topic └── UserNotificationTopic.php ├── composer.json ├── diagram.png └── screen.png /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | level(Symfony\CS\FixerInterface::SYMFONY_LEVEL) 5 | ->fixers(array( 6 | 'ordered_use', 7 | 'multiline_spaces_before_semicolon', 8 | 'concat_with_spaces' 9 | )) 10 | ->finder( 11 | Symfony\CS\Finder\DefaultFinder::create() 12 | ->exclude(['vendor', 'app/cache']) 13 | ->in(__DIR__) 14 | ) 15 | ; -------------------------------------------------------------------------------- /Command/ServerCommand.php: -------------------------------------------------------------------------------- 1 | server = $server; 30 | $this->pubsubConfig = $pubsubConfig; 31 | 32 | parent::__construct(); 33 | } 34 | 35 | protected function configure() 36 | { 37 | $this 38 | ->setName('gos:notification:server') 39 | ->setDescription('Starts the notification server') 40 | ->addOption('profile', 'p', InputOption::VALUE_NONE, 'Profiling server'); 41 | } 42 | 43 | /** 44 | * @param InputInterface $input 45 | * @param OutputInterface $output 46 | */ 47 | protected function execute(InputInterface $input, OutputInterface $output) 48 | { 49 | $this->server->launch( 50 | $this->pubsubConfig['host'], 51 | $this->pubsubConfig['port'], 52 | $input->getOption('profile') 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Context/NotificationContext.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class NotificationContext implements NotificationContextInterface 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Context/NotificationContextInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class PusherIdentity 11 | { 12 | /** 13 | * @var string 14 | */ 15 | protected $type; 16 | 17 | /** 18 | * @var string 19 | */ 20 | protected $identifier; 21 | 22 | /** 23 | * @param string $type 24 | * @param string $identifier 25 | */ 26 | public function __construct($type, $identifier) 27 | { 28 | $this->type = $type; 29 | $this->identifier = $identifier; 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function __toString() 36 | { 37 | return $this->getIdentity(); 38 | } 39 | 40 | /** 41 | * @param UserInterface $user 42 | * 43 | * @return PusherIdentity|$this 44 | */ 45 | public static function fromAccount(UserInterface $user) 46 | { 47 | return new self('user', $user->getUsername()); 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getType() 54 | { 55 | return $this->type; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getIdentifier() 62 | { 63 | return $this->identifier; 64 | } 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function getIdentity() 70 | { 71 | return $this->type . '#' . $this->identifier; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /DependencyInjection/CompilerPass/PusherCompilerPass.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class PusherCompilerPass implements CompilerPassInterface 13 | { 14 | /** 15 | * @param ContainerBuilder $container 16 | */ 17 | public function process(ContainerBuilder $container) 18 | { 19 | $definition = $container->getDefinition('gos_notification.pusher.registry'); 20 | $taggedServices = $container->findTaggedServiceIds('gos_notification.pusher'); 21 | 22 | foreach ($taggedServices as $id => $attributes) { 23 | $definition->addMethodCall('addPusher', [new Reference($id)]); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class Configuration implements ConfigurationInterface 12 | { 13 | /** 14 | * {@inheritDoc} 15 | */ 16 | public function getConfigTreeBuilder() 17 | { 18 | $treeBuilder = new TreeBuilder(); 19 | $rootNode = $treeBuilder->root('gos_notification'); 20 | 21 | $rootNode->children() 22 | ->arrayNode('pusher') 23 | ->prototype('scalar') 24 | ->end() 25 | ->end() 26 | ->scalarNode('fetcher') 27 | ->cannotBeEmpty() 28 | ->isRequired() 29 | ->end() 30 | ->scalarNode('publisher') 31 | ->cannotBeEmpty() 32 | ->isRequired() 33 | ->end() 34 | ->arrayNode('class') 35 | ->addDefaultsIfNotSet() 36 | ->children() 37 | ->scalarNode('notification') 38 | ->defaultValue('Gos\Bundle\NotificationBundle\Model\Notification') 39 | ->end() 40 | ->scalarNode('notification_context') 41 | ->defaultValue('Gos\Bundle\NotificationBundle\Context\NotificationContext') 42 | ->end() 43 | ->end() 44 | ->end() 45 | ->arrayNode('pubsub_server') 46 | ->children() 47 | ->scalarNode('type')->end() 48 | ->arrayNode('config') 49 | ->prototype('scalar') 50 | ->end() 51 | ->end() 52 | ->end() 53 | ->end() 54 | ->end(); 55 | 56 | return $treeBuilder; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /DependencyInjection/GosNotificationExtension.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class GosNotificationExtension extends Extension implements PrependExtensionInterface 17 | { 18 | /** 19 | * {@inheritDoc} 20 | */ 21 | public function load(array $configs, ContainerBuilder $container) 22 | { 23 | $loader = new Loader\YamlFileLoader( 24 | $container, 25 | new FileLocator(__DIR__ . '/../Resources/config/services') 26 | ); 27 | 28 | $loader->load('services.yml'); 29 | 30 | $configuration = new Configuration(); 31 | $configs = $this->processConfiguration($configuration, $configs); 32 | 33 | $container->setParameter('gos_notification.pubsub_server.type', $configs['pubsub_server']['type']); 34 | $container->setParameter('gos_notification.pubsub_server.config', $configs['pubsub_server']['config']); 35 | 36 | //class 37 | $container->setParameter('gos_notification.notification_class', $configs['class']['notification']); 38 | $container->setParameter('gos_notification.notification_context_class', $configs['class']['notification_context']); 39 | 40 | //pusher 41 | if (isset($configs['pusher']) && !empty($configs['pusher'])) { 42 | $pusherRegistryDef = $container->getDefinition('gos_notification.pusher.registry'); 43 | 44 | foreach ($configs['pusher'] as $pusher) { 45 | $pusherRegistryDef->addMethodCall('addPusher', array(new Reference(ltrim($pusher, '@')))); 46 | } 47 | } 48 | 49 | //fetcher 50 | $container->setAlias('gos_notification.fetcher', ltrim($configs['fetcher'], '@')); 51 | 52 | //publisher 53 | $container->setAlias('gos_notification.publisher', ltrim($configs['publisher'], '@')); 54 | } 55 | 56 | /** 57 | * @param ContainerBuilder $container 58 | * 59 | * @throws \Exception 60 | */ 61 | public function prepend(ContainerBuilder $container) 62 | { 63 | $bundles = $container->getParameter('kernel.bundles'); 64 | 65 | if (isset($bundles['MonologBundle'])) { 66 | $monologConfig = array( 67 | 'channels' => array('notification'), 68 | 'handlers' => array( 69 | 'notification' => array( 70 | 'type' => 'stream', 71 | 'path' => '%kernel.logs_dir%/notification.log', 72 | 'channels' => 'notification', 73 | ), 74 | 'notification_cli' => array( 75 | 'type' => 'console', 76 | 'verbosity_levels' => array( 77 | 'VERBOSITY_NORMAL' => true === $container->getParameter('kernel.debug') ? Logger::DEBUG : Logger::INFO, 78 | ), 79 | 'channels' => 'notification', 80 | ), 81 | ), 82 | ); 83 | 84 | $container->prependExtensionConfig('monolog', $monologConfig); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Event/NotificationEvents.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | final class NotificationEvents 9 | { 10 | const NOTIFICATION_PUBLISHED = 'gos_notification.notification.published'; 11 | const NOTIFICATION_PUSHED = 'gos_notification.notification.pushed'; 12 | 13 | const NOTIFICATION_CONSUMED = 'gos_notification.notification.consumed'; 14 | } 15 | -------------------------------------------------------------------------------- /Event/NotificationPublishedEvent.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class NotificationPublishedEvent extends Event 15 | { 16 | /** 17 | * @var NotificationInterface 18 | */ 19 | protected $notification; 20 | 21 | /** 22 | * @var NotificationContextInterface|null 23 | */ 24 | protected $context; 25 | 26 | /** 27 | * @var MessageInterface 28 | */ 29 | protected $message; 30 | 31 | /** @var PubSubRequest */ 32 | protected $request; 33 | 34 | /** 35 | * @param MessageInterface $message 36 | * @param NotificationInterface $notification 37 | * @param NotificationContextInterface|null $context 38 | * @param PubSubRequest $request 39 | */ 40 | public function __construct( 41 | MessageInterface $message, 42 | NotificationInterface $notification, 43 | NotificationContextInterface $context = null, 44 | PubSubRequest $request 45 | ) { 46 | $this->message = $message; 47 | $this->notification = $notification; 48 | $this->context = $context; 49 | $this->request = $request; 50 | } 51 | 52 | /** 53 | * @return NotificationContextInterface 54 | */ 55 | public function getContext() 56 | { 57 | return $this->context; 58 | } 59 | 60 | /** 61 | * @return NotificationInterface 62 | */ 63 | public function getNotification() 64 | { 65 | return $this->notification; 66 | } 67 | 68 | /** 69 | * @return MessageInterface 70 | */ 71 | public function getMessage() 72 | { 73 | return $this->message; 74 | } 75 | 76 | /** 77 | * @return PubSubRequest 78 | */ 79 | public function getRequest() 80 | { 81 | return $this->request; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Event/NotificationPushedEvent.php: -------------------------------------------------------------------------------- 1 | message = $message; 52 | $this->notification = $notification; 53 | $this->context = $context; 54 | $this->pusher = $pusher; 55 | } 56 | 57 | /** 58 | * @return MessageInterface 59 | */ 60 | public function getMessage() 61 | { 62 | return $this->message; 63 | } 64 | 65 | /** 66 | * @return NotificationInterface 67 | */ 68 | public function getNotification() 69 | { 70 | return $this->notification; 71 | } 72 | 73 | /** 74 | * @return NotificationContextInterface 75 | */ 76 | public function getContext() 77 | { 78 | return $this->context; 79 | } 80 | 81 | /** 82 | * @return PusherInterface 83 | */ 84 | public function getPusher() 85 | { 86 | return $this->pusher; 87 | } 88 | 89 | /** 90 | * @return PubSubRequest 91 | */ 92 | public function getRequest() 93 | { 94 | return $this->request; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Exception/NotFoundNotificationException.php: -------------------------------------------------------------------------------- 1 | client = $client; 41 | $this->serializer = $serializer; 42 | $this->logger = null === $logger ? new NullLogger() : $logger; 43 | 44 | //Command to enable to retrieve notification by uuid 45 | $client->getProfile()->defineCommand('lidxof', new IndexOfElement()); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function multipleFetch(array $channels, $start, $end) 52 | { 53 | $notifications = []; 54 | 55 | foreach ($channels as $url) { 56 | $messages = $this->client->lrange($url, $start, $end); 57 | 58 | foreach ($messages as $message) { 59 | $notifications[$url][] = $this->serializer->deserialize($message); 60 | } 61 | } 62 | 63 | return $notifications; 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function fetch($channel, $start, $end) 70 | { 71 | $messages = $this->client->lrange($channel, $start, $end); 72 | $notifications = []; 73 | 74 | foreach ($messages as $key => $message) { 75 | $notifications[] = $this->serializer->deserialize($message); 76 | } 77 | 78 | return $notifications; 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function multipleCount(array $channels, array $options = []) 85 | { 86 | $counter = array(); 87 | $total = 0; 88 | 89 | foreach ($channels as $url) { 90 | $count = $this->client->get($url . '-counter'); 91 | $counter[$url] = (int) $count; 92 | $total += $count; 93 | } 94 | 95 | $counter['total'] = $total; 96 | 97 | return $counter; 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function count($channel, array $options = []) 104 | { 105 | return $this->client->get($channel . '-counter'); 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | public function getNotification($channel, $uuid) 112 | { 113 | return $this->doGetNotification($channel, $uuid); 114 | } 115 | 116 | /** 117 | * @param string $url 118 | * @param string $uuid 119 | * 120 | * @return NotificationInterface 121 | * 122 | * @throws NotFoundNotificationException 123 | */ 124 | protected function doGetNotification($url, $uuid) 125 | { 126 | $index = $this->client->lidxof($url, 'uuid', $uuid); 127 | 128 | if ($index === -1) { 129 | throw new NotFoundNotificationException($uuid); 130 | } 131 | 132 | $message = $this->client->lindex($url, $index); 133 | 134 | $notification = $this->serializer->deserialize($message); 135 | 136 | return $notification; 137 | } 138 | 139 | /** 140 | * {@inheritdoc} 141 | */ 142 | public function markAsViewed($channel, $uuidOrNotification, $force = false) 143 | { 144 | if ($uuidOrNotification instanceof NotificationInterface) { 145 | $uuid = $uuidOrNotification->getUuid(); 146 | } else { 147 | $uuid = $uuidOrNotification; 148 | } 149 | 150 | $notification = $this->doGetNotification($channel, $uuid); 151 | 152 | if (true === $force) { 153 | $notification->setViewedAt(new \DateTime()); 154 | } else { 155 | if (null === $notification->getViewedAt()) { 156 | $notification->setViewedAt(new \DateTime()); 157 | } 158 | } 159 | 160 | $index = $this->client->lidxof($channel, 'uuid', $uuid); 161 | 162 | $this->client->pipeline(function ($pipe) use ($channel, $index, $notification) { 163 | $pipe->lset($channel, $index, $this->serializer->serialize($notification)); 164 | $pipe->decr($channel . '-counter'); 165 | }); 166 | 167 | return true; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /GosNotificationBundle.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class GosNotificationBundle extends Bundle 13 | { 14 | /** 15 | * @param ContainerBuilder $container 16 | */ 17 | public function build(ContainerBuilder $container) 18 | { 19 | $container->addCompilerPass(new PusherCompilerPass()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Johann Saunier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Listener/NotificationListener.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class NotificationListener 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Model/Message/Message.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class Message implements MessageInterface 9 | { 10 | /** 11 | * @var string 12 | */ 13 | protected $kind; 14 | 15 | /** 16 | * @var string 17 | */ 18 | protected $channel; 19 | 20 | /** 21 | * @var string 22 | */ 23 | protected $payload; 24 | 25 | /** 26 | * @param string $kind 27 | * @param string $channel 28 | * @param string $payload 29 | */ 30 | public function __construct($kind, $channel, $payload) 31 | { 32 | $this->kind = $kind; 33 | $this->channel = $channel; 34 | $this->payload = $payload; 35 | } 36 | 37 | /** 38 | * @return string 39 | */ 40 | public function getKind() 41 | { 42 | return $this->kind; 43 | } 44 | 45 | /** 46 | * @return string 47 | */ 48 | public function getChannel() 49 | { 50 | return $this->channel; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public function getPayload() 57 | { 58 | return $this->payload; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Model/Message/MessageInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class PatternMessage implements MessageInterface 9 | { 10 | /** 11 | * @var string 12 | */ 13 | protected $kind; 14 | 15 | /** 16 | * @var string 17 | */ 18 | protected $pattern; 19 | 20 | /** 21 | * @var string 22 | */ 23 | protected $channel; 24 | 25 | /** 26 | * @var string 27 | */ 28 | protected $payload; 29 | 30 | /** 31 | * @param string $kind 32 | * @param string $pattern 33 | * @param string $channel 34 | * @param string $payload 35 | */ 36 | public function __construct($kind, $pattern, $channel, $payload) 37 | { 38 | $this->kind = $kind; 39 | $this->pattern = $pattern; 40 | $this->channel = $channel; 41 | $this->payload = $payload; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getKind() 48 | { 49 | return $this->kind; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function getPattern() 56 | { 57 | return $this->pattern; 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getChannel() 64 | { 65 | return $this->channel; 66 | } 67 | 68 | /** 69 | * @return string 70 | */ 71 | public function getPayload() 72 | { 73 | return $this->payload; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Model/Notification.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class Notification implements NotificationInterface 9 | { 10 | const TYPE_INFO = 'info'; 11 | const TYPE_ERROR = 'error'; 12 | const TYPE_SUCCESS = 'success'; 13 | const TYPE_WARNING = 'warning'; 14 | 15 | /** @var string */ 16 | protected $uuid; 17 | 18 | /** @var string */ 19 | protected $type; 20 | 21 | /** @var string */ 22 | protected $icon; 23 | 24 | /** @var \DateTime */ 25 | protected $viewedAt; 26 | 27 | /** @var \DateTime */ 28 | protected $createdAt; 29 | 30 | /** @var string */ 31 | protected $title; 32 | 33 | /** @var string */ 34 | protected $content; 35 | 36 | /** @var string */ 37 | protected $link; 38 | 39 | /** @var array */ 40 | protected $extra; 41 | 42 | /** @var int */ 43 | protected $timeout; 44 | 45 | /** @var string */ 46 | protected $channel; 47 | 48 | public function __construct() 49 | { 50 | $this->createdAt = new \DateTime(); 51 | $this->uuid = $this->generateUuid(); 52 | $this->extra = []; 53 | $this->timeout = 5000; 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | public function getChannel() 60 | { 61 | return $this->channel; 62 | } 63 | 64 | /** 65 | * @param string $channel 66 | */ 67 | public function setChannel($channel) 68 | { 69 | $this->channel = $channel; 70 | } 71 | 72 | /** 73 | * @param $key 74 | * @param $value 75 | */ 76 | public function addExtra($key, $value) 77 | { 78 | $this->extra[$key] = $value; 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getLink() 85 | { 86 | return $this->link; 87 | } 88 | 89 | /** 90 | * @param string $link 91 | */ 92 | public function setLink($link) 93 | { 94 | $this->link = $link; 95 | } 96 | 97 | /** 98 | * @return array 99 | */ 100 | public function getExtra() 101 | { 102 | return $this->extra; 103 | } 104 | 105 | /** 106 | * @param array $extra 107 | */ 108 | public function setExtra($extra) 109 | { 110 | $this->extra = $extra; 111 | } 112 | 113 | /** 114 | * @return int 115 | */ 116 | public function getTimeout() 117 | { 118 | return $this->timeout; 119 | } 120 | 121 | /** 122 | * @param int $timeout 123 | */ 124 | public function setTimeout($timeout) 125 | { 126 | $this->timeout = $timeout; 127 | } 128 | 129 | /** 130 | * UUID v4. 131 | * 132 | * @return string 133 | */ 134 | protected function generateUuid() 135 | { 136 | return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', 137 | mt_rand(0, 0xffff), mt_rand(0, 0xffff), 138 | mt_rand(0, 0xffff), 139 | mt_rand(0, 0x0fff) | 0x4000, 140 | mt_rand(0, 0x3fff) | 0x8000, 141 | mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) 142 | ); 143 | } 144 | 145 | /** 146 | * @return string 147 | */ 148 | public function getUuid() 149 | { 150 | return $this->uuid; 151 | } 152 | 153 | /** 154 | * @return string 155 | */ 156 | public function getTitle() 157 | { 158 | return $this->title; 159 | } 160 | 161 | /** 162 | * @param string $title 163 | */ 164 | public function setTitle($title) 165 | { 166 | $this->title = $title; 167 | } 168 | 169 | /** 170 | * @return string 171 | */ 172 | public function getType() 173 | { 174 | return $this->type; 175 | } 176 | 177 | /** 178 | * @param string $type 179 | */ 180 | public function setType($type) 181 | { 182 | $this->type = $type; 183 | } 184 | 185 | /** 186 | * @return string 187 | */ 188 | public function getIcon() 189 | { 190 | return $this->icon; 191 | } 192 | 193 | /** 194 | * @param string $icon 195 | */ 196 | public function setIcon($icon) 197 | { 198 | $this->icon = $icon; 199 | } 200 | 201 | /** 202 | * @return \DateTime 203 | */ 204 | public function getViewedAt() 205 | { 206 | return $this->viewedAt; 207 | } 208 | 209 | /** 210 | * @param \DateTime|string $viewedAt 211 | */ 212 | public function setViewedAt($viewedAt = null) 213 | { 214 | if ($viewedAt instanceof \DateTime) { 215 | $this->viewedAt = $viewedAt; 216 | } else { 217 | $this->viewedAt = \DateTime::createFromFormat(\DateTime::W3C, $viewedAt); 218 | } 219 | } 220 | 221 | /** 222 | * @return \DateTime 223 | */ 224 | public function getCreatedAt() 225 | { 226 | return $this->createdAt; 227 | } 228 | 229 | /** 230 | * @param \DateTime|string $createdAt 231 | */ 232 | public function setCreatedAt($createdAt) 233 | { 234 | if ($createdAt instanceof \DateTime) { 235 | $this->createdAt = $createdAt; 236 | } else { 237 | $this->createdAt = \DateTime::createFromFormat(\DateTime::W3C, $createdAt); 238 | } 239 | } 240 | 241 | /** 242 | * @return string 243 | */ 244 | public function getContent() 245 | { 246 | return $this->content; 247 | } 248 | 249 | /** 250 | * @param string $content 251 | */ 252 | public function setContent($content) 253 | { 254 | $this->content = $content; 255 | } 256 | 257 | /**** Transformer Methods *****/ 258 | 259 | /** 260 | * @return array 261 | */ 262 | public function toArray() 263 | { 264 | return array( 265 | 'uuid' => $this->uuid, 266 | 'type' => $this->type, 267 | 'icon' => $this->icon, 268 | 'viewed_at' => $this->viewedAt !== null ? $this->viewedAt->format(\DateTime::W3C) : null, 269 | 'created_at' => $this->createdAt->format(\DateTime::W3C), 270 | 'content' => $this->content, 271 | 'title' => $this->title, 272 | 'link' => $this->link, 273 | 'extra' => $this->extra, 274 | 'timeout' => $this->timeout, 275 | 'channel' => $this->channel, 276 | ); 277 | } 278 | 279 | /** 280 | * (PHP 5 >= 5.4.0)
281 | * Specify data which should be serialized to JSON. 282 | * 283 | * @link http://php.net/manual/en/jsonserializable.jsonserialize.php 284 | * 285 | * @return mixed data which can be serialized by json_encode, 286 | * which is a value of any type other than a resource. 287 | */ 288 | public function jsonSerialize() 289 | { 290 | return $this->toArray(); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /Model/NotificationInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface NotificationInterface extends \JsonSerializable 9 | { 10 | /** 11 | * @param $key 12 | * @param $value 13 | */ 14 | public function addExtra($key, $value); 15 | 16 | /** 17 | * @return string 18 | */ 19 | public function getLink(); 20 | 21 | /** 22 | * @param string $link 23 | */ 24 | public function setLink($link); 25 | 26 | /** 27 | * @return array 28 | */ 29 | public function getExtra(); 30 | 31 | /** 32 | * @param array $extra 33 | */ 34 | public function setExtra($extra); 35 | 36 | /** 37 | * @return int 38 | */ 39 | public function getTimeout(); 40 | 41 | /** 42 | * @param int $timeout 43 | */ 44 | public function setTimeout($timeout); 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function getUuid(); 50 | 51 | /** 52 | * @return string 53 | */ 54 | public function getTitle(); 55 | 56 | /** 57 | * @param string $title 58 | */ 59 | public function setTitle($title); 60 | 61 | /** 62 | * @return string 63 | */ 64 | public function getType(); 65 | 66 | /** 67 | * @param string $type 68 | */ 69 | public function setType($type); 70 | 71 | /** 72 | * @return string 73 | */ 74 | public function getIcon(); 75 | 76 | /** 77 | * @param string $icon 78 | */ 79 | public function setIcon($icon); 80 | 81 | /** 82 | * @return \DateTime 83 | */ 84 | public function getViewedAt(); 85 | 86 | /** 87 | * @param \DateTime $viewedAt 88 | */ 89 | public function setViewedAt($viewedAt = null); 90 | 91 | /** 92 | * @return \DateTime|string 93 | */ 94 | public function getCreatedAt(); 95 | 96 | /** 97 | * @param \DateTime|string $createdAt 98 | */ 99 | public function setCreatedAt($createdAt); 100 | 101 | /** 102 | * @return string 103 | */ 104 | public function getContent(); 105 | 106 | /** 107 | * @param string $content 108 | */ 109 | public function setContent($content); 110 | 111 | /** 112 | * @return array 113 | */ 114 | public function toArray(); 115 | 116 | /** 117 | * @return string 118 | */ 119 | public function getChannel(); 120 | 121 | /** 122 | * @param string $url 123 | */ 124 | public function setChannel($url); 125 | } 126 | -------------------------------------------------------------------------------- /NotificationCenter.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class NotificationCenter implements NotificationManipulatorInterface 14 | { 15 | /** 16 | * @var PublisherInterface 17 | */ 18 | protected $publisher; 19 | 20 | /** 21 | * @var FetcherInterface 22 | */ 23 | protected $fetcher; 24 | 25 | /** 26 | * @param PublisherInterface $publisher 27 | * @param FetcherInterface $fetcher 28 | */ 29 | public function __construct( 30 | PublisherInterface $publisher, 31 | FetcherInterface $fetcher 32 | ) { 33 | $this->publisher = $publisher; 34 | $this->fetcher = $fetcher; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function fetch($channel, $start, $end) 41 | { 42 | return $this->fetcher->fetch($channel, $start, $end); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function publish($channel, NotificationInterface $notification, NotificationContextInterface $context = null) 49 | { 50 | return $this->publisher->publish($channel, $notification, $context); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function count($channel, array $options = []) 57 | { 58 | return $this->fetcher->count($channel, $options); 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function getNotification($channel, $uuid) 65 | { 66 | return $this->fetcher->getNotification($channel, $uuid); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function markAsViewed($channel, $uuidOrNotification, $force = false) 73 | { 74 | return $this->fetcher->markAsViewed($channel, $uuidOrNotification, $force); 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function multipleFetch(array $channels, $start, $end) 81 | { 82 | return $this->fetcher->multipleFetch($channels, $start, $end); 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | */ 88 | public function multipleCount(array $channels, array $options = []) 89 | { 90 | return $this->fetcher->multipleCount($channels, $options); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /NotificationManipulatorInterface.php: -------------------------------------------------------------------------------- 1 | notificationCenter = $notificationCenter; 31 | $this->clientStorage = $clientStorage; 32 | } 33 | 34 | /** 35 | * @param string $channel 36 | * 37 | * @return string 38 | */ 39 | protected function transliterateChannel($channel) 40 | { 41 | return str_replace('/', ':', $channel); 42 | } 43 | 44 | /** 45 | * @param ConnectionInterface $conn 46 | * @param $params 47 | * 48 | * @return RpcResponse 49 | */ 50 | public function fetch(ConnectionInterface $conn, WampRequest $request, Array $params) 51 | { 52 | $start = $params['start']; 53 | $end = $params['end']; 54 | $channel = $this->transliterateChannel($params['channel']); 55 | 56 | if (is_array($channel)) { 57 | $result = $this->notificationCenter->multipleFetch($channel, $start, $end); 58 | } else { 59 | $result = $this->notificationCenter->fetch($channel, $start, $end); 60 | } 61 | 62 | return new RpcResponse($result); 63 | } 64 | 65 | /** 66 | * @param ConnectionInterface $conn 67 | * @param array $params 68 | * 69 | * @return RpcResponse 70 | */ 71 | public function count(ConnectionInterface $conn, WampRequest $request, Array $params) 72 | { 73 | $options = $params['options']; 74 | $channel = $this->transliterateChannel($params['channel']); 75 | 76 | if (is_array($channel)) { 77 | $result = $this->notificationCenter->multipleCount($channel, $options); 78 | } else { 79 | $result = $this->notificationCenter->count($channel, $options); 80 | } 81 | 82 | return new RpcResponse($result); 83 | } 84 | 85 | /** 86 | * @param ConnectionInterface $conn 87 | * @param array $params 88 | * 89 | * @return RpcResponse 90 | */ 91 | public function getNotification(ConnectionInterface $conn, WampRequest $request, Array $params) 92 | { 93 | $uuid = $params['uuid']; 94 | $channel = $this->transliterateChannel($params['channel']); 95 | 96 | return new RpcResponse($channel, $this->notificationCenter->getNotification($channel, $uuid)); 97 | } 98 | 99 | /** 100 | * @param ConnectionInterface $conn 101 | * @param array $params 102 | * 103 | * @return RpcResponse 104 | */ 105 | public function markAsViewed(ConnectionInterface $conn, WampRequest $request, Array $params) 106 | { 107 | $channel = $this->transliterateChannel($params['channel']); 108 | $uuid = $params['uuid']; 109 | 110 | if (isset($params['force'])) { 111 | $force = (bool) $params['force']; 112 | $result = $this->notificationCenter->markAsViewed($channel, $uuid, $force); 113 | } else { 114 | $result = $this->notificationCenter->markAsViewed($channel, $uuid); 115 | } 116 | 117 | return new RpcResponse($result); 118 | } 119 | 120 | /** 121 | * @return string 122 | */ 123 | public function getName() 124 | { 125 | return 'gos.notification.rpc'; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Processor/ProcessorInterface.php: -------------------------------------------------------------------------------- 1 | redis = $redis; 30 | $this->logger = null === $logger ? new NullLogger() : $logger; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function publish($channel, NotificationInterface $notification, NotificationContextInterface $context = null) 37 | { 38 | $this->logger->info(sprintf( 39 | 'push %s into %s', 40 | $notification->getTitle(), 41 | $channel 42 | ), $notification->toArray()); 43 | 44 | $data = []; 45 | $data['notification'] = $notification; 46 | 47 | if (null !== $context) { 48 | $data['context'] = $context; 49 | } 50 | 51 | $message = json_encode($data); 52 | 53 | $this->redis->publish($channel, $message); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Pusher/AbstractPusher.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function push(MessageInterface $message, NotificationInterface $notification, PubSubRequest $request, NotificationContextInterface $context = null) 36 | { 37 | $route = $request->getRoute(); 38 | $matrix = []; 39 | 40 | foreach ($request->getAttributes()->all() as $name => $value) { 41 | if (!isset($this->processors[$name])) { 42 | throw new \Exception(sprintf('Missing processor for %s on route "%s"', $name, $request->getRoute())); 43 | } 44 | 45 | /** @var ProcessorInterface $processor */ 46 | $processor = $this->processors[$name]; 47 | 48 | //attribute is wildcarded 49 | if (in_array($request->getAttributes()->get($name, false), ['*', 'all']) && 50 | isset($route->getRequirements()[$name]['wildcard']) && 51 | true === $route->getRequirements()[$name]['wildcard'] 52 | ) { 53 | $matrix[$name] = $processor->process(true, $this->getAlias(), $notification, $request); 54 | } else { //he is not 55 | $matrix[$name] = $processor->process(false, $this->getAlias(), $notification, $request); 56 | } 57 | } 58 | 59 | $matrix = $this->fixMapIndex($route, $matrix); 60 | 61 | $this->doPush($message, $notification, $request, $matrix, $context); 62 | 63 | $this->eventDispatcher->dispatch(NotificationEvents::NOTIFICATION_PUSHED, new NotificationPushedEvent($message, $notification, $request, $context, $this)); 64 | } 65 | 66 | /** 67 | * @param MessageInterface $message 68 | * @param NotificationInterface $notification 69 | * @param PubSubRequest $request 70 | * @param array $matrix 71 | * @param NotificationContextInterface $context 72 | */ 73 | protected function doPush( 74 | MessageInterface $message, 75 | NotificationInterface $notification, 76 | PubSubRequest $request, 77 | array $matrix, 78 | NotificationContextInterface $context = null 79 | ) { 80 | //override it ! 81 | } 82 | 83 | /** 84 | * @param RouteInterface $route 85 | * @param array $matrix 86 | * 87 | * @return array 88 | */ 89 | protected function fixMapIndex(RouteInterface $route, Array &$matrix) 90 | { 91 | $pattern = implode('|', array_keys($route->getRequirements())); 92 | $matches = []; 93 | 94 | preg_match_all('#' . $pattern . '#', $route->getPattern(), $matches); 95 | 96 | uksort($matrix, function ($key) use ($matches) { 97 | foreach ($matches[0] as $order => $attributeName) { 98 | if ($attributeName === $key) { 99 | return $order; 100 | } 101 | } 102 | }); 103 | 104 | return $matrix; 105 | } 106 | 107 | /** 108 | * @param array $data 109 | * @param array $all 110 | * @param string $groupName 111 | * @param array $group 112 | * @param null $value 113 | * @param int $i 114 | * 115 | * @see http://fr.wikipedia.org/wiki/Matrice_de_permutation 116 | * 117 | * @return array 118 | */ 119 | protected function generateMatrixPermutations( 120 | array $data, 121 | array &$all = array(), 122 | $groupName = '', 123 | array $group = array(), 124 | $value = null, 125 | $i = 0 126 | ) { 127 | $keys = array_keys($data); 128 | 129 | if (isset($value) === true) { 130 | $group[$groupName] = $value; 131 | } 132 | 133 | if ($i >= count($data)) { 134 | array_push($all, $group); 135 | } else { 136 | $currentKey = $keys[$i]; 137 | $currentElement = $data[$currentKey]; 138 | 139 | if (is_array($currentElement)) { 140 | foreach ($currentElement as $groupName => $val) { 141 | $this->generateMatrixPermutations($data, $all, $currentKey, $group, $val, $i + 1); 142 | } 143 | } else { 144 | $this->generateMatrixPermutations($data, $all, $currentKey, $group, $currentElement, $i + 1); 145 | } 146 | } 147 | 148 | return $all; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Pusher/ProcessorDelegate.php: -------------------------------------------------------------------------------- 1 | processors[$attribute] = $processor; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Pusher/PusherInterface.php: -------------------------------------------------------------------------------- 1 | pushers = array(); 18 | } 19 | 20 | /** 21 | * @param PusherInterface $pusher 22 | */ 23 | public function addPusher(PusherInterface $pusher) 24 | { 25 | $this->pushers[$pusher->getAlias()] = $pusher; 26 | } 27 | 28 | /** 29 | * @param array $specificPushers 30 | * 31 | * @return PusherInterface[] 32 | */ 33 | public function getPushers(Array $specificPushers = null) 34 | { 35 | if (null === $specificPushers || empty($specificPushers)) { 36 | return $this->pushers; 37 | } 38 | 39 | $pushers = array(); 40 | 41 | foreach ($this->pushers as $pusher) { 42 | if (in_array($pusher->getAlias(), $specificPushers)) { 43 | $pushers[] = $pusher; 44 | } 45 | } 46 | 47 | return $pushers; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Pusher/RedisPusher.php: -------------------------------------------------------------------------------- 1 | client = $client; 37 | $this->router = $router; 38 | } 39 | 40 | /** 41 | * @param RouteInterface $route 42 | * @param array $matrix 43 | * 44 | * @return array 45 | */ 46 | protected function generateRoutes(RouteInterface $route, array $matrix) 47 | { 48 | $channels = []; 49 | foreach ($this->generateMatrixPermutations($matrix) as $parameters) { 50 | $channels[] = $this->router->generate((string) $route, $parameters); 51 | } 52 | 53 | return $channels; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | protected function doPush( 60 | MessageInterface $message, 61 | NotificationInterface $notification, 62 | PubSubRequest $request, 63 | Array $matrix, 64 | NotificationContextInterface $context = null 65 | ) { 66 | $pipe = $this->client->pipeline(); 67 | 68 | foreach ($this->generateRoutes($request->getRoute(), $matrix) as $channel) { 69 | $notification->setChannel($channel); 70 | $pipe->lpush($channel, json_encode($notification->toArray())); 71 | $pipe->incr($channel . '-counter'); 72 | } 73 | 74 | $pipe->execute(); 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function getAlias() 81 | { 82 | return static::ALIAS; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Pusher/WebsocketPusher.php: -------------------------------------------------------------------------------- 1 | serverHost = $serverHost; 58 | $this->serverPort = $serverPort; 59 | $this->router = $router; 60 | $this->logger = null === $logger ? new NullLogger() : $logger; 61 | $this->connected = false; 62 | } 63 | 64 | /** 65 | * @param RouteInterface $route 66 | * @param array $matrix 67 | * 68 | * @return array 69 | */ 70 | protected function generateRoutes(RouteInterface $route, array $matrix) 71 | { 72 | $channels = []; 73 | foreach ($this->generateMatrixPermutations($matrix) as $parameters) { 74 | $channels[] = $this->router->generate((string) $route, $parameters); 75 | } 76 | 77 | return $channels; 78 | } 79 | 80 | /** 81 | * @param MessageInterface $message 82 | * @param NotificationInterface $notification 83 | * @param PubSubRequest $request 84 | * @param array $matrix 85 | * @param NotificationContextInterface $context 86 | * 87 | * @throws \Gos\Component\WebSocketClient\Exception\BadResponseException 88 | */ 89 | protected function doPush( 90 | MessageInterface $message, 91 | NotificationInterface $notification, 92 | PubSubRequest $request, 93 | array $matrix, 94 | NotificationContextInterface $context = null 95 | ) { 96 | if(false === $this->connected){ 97 | $this->ws = new Client($this->serverHost, $this->serverPort); 98 | $this->ws->connect(); 99 | } 100 | 101 | foreach ($this->generateRoutes($request->getRoute(), $matrix) as $channel) { 102 | $notification->setChannel($channel); 103 | $this->ws->publish($channel, json_encode($notification)); 104 | } 105 | } 106 | 107 | /** 108 | * @return bool 109 | */ 110 | public function isAvailable() 111 | { 112 | $pingger = new PingBack($this->serverHost, $this->serverPort); 113 | 114 | return $pingger->ping(); 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | */ 120 | public function getAlias() 121 | { 122 | return static::ALIAS; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gos Notification Bundle 2 | ===================== 3 | 4 | [![Join the chat at https://gitter.im/GeniusesOfSymfony/NotificationBundle](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/GeniusesOfSymfony/NotificationBundle?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | **This bundle is under active development, don't ship it in production :)** 7 | 8 | About 9 | -------------- 10 | Gos Notification is a Symfony2 Bundle designed to bring real time notification system in your application architecture. 11 | Allow to use multiple pusher like websocket or redis. The notification system is based on pubsub architecture. 12 | 13 | How it work 14 | ----------- 15 | 16 | ![diagram.png](diagram.png) 17 | 18 | Screenshot 19 | ---------- 20 | 21 | ![screen.png](screen.png) 22 | 23 | 24 | **More documentation will coming soon** 25 | -------------------------------------------------------------------------------- /Redis/IndexOfElement.php: -------------------------------------------------------------------------------- 1 | 24 | ``` 25 | 26 | Development 27 | ---------- 28 | 29 | ```cmd 30 | gulp //watch by default 31 | gulp watch //watch less & coffee 32 | gulp less //compile less 33 | gulp browserify //compile angular coffee app 34 | gulp serve //compile browersify and less 35 | ``` 36 | 37 | Production 38 | ---------- 39 | 40 | ```cmd 41 | gulp serve --production //use it also when you commit dist folder for PR to update properly files. 42 | ``` 43 | 44 | **This is currently in progress, there are still a lot of work !** -------------------------------------------------------------------------------- /Resources/public/js/notification/dist/notification-widget.css: -------------------------------------------------------------------------------- 1 | .button-default,.show-notifications{position:relative}@font-face{font-family:Lato;font-style:normal;font-weight:700;src:local('Lato Bold'),local('Lato-Bold'),url(http://fonts.gstatic.com/s/lato/v11/DvlFBScY1r-FMtZSYIYoYw.ttf)format('truetype')}*,:after,:before{box-sizing:border-box}.button-default{-webkit-transition:.25s ease-out .1s color;transition:.25s ease-out .1s color;background:0 0;border:none;cursor:pointer;margin:0;outline:0}.show-notifications.active #icon-bell,.show-notifications:focus #icon-bell,.show-notifications:hover #icon-bell{fill:#34495e}.show-notifications #icon-bell{fill:#7f8c8d}.show-notifications .notifications-count{border-radius:50%;background:#3498db;color:#fefefe;font:400 .85em Lato;height:16px;line-height:1.75em;position:absolute;right:2px;text-align:center;top:-2px;width:16px}.show-notifications.active~.notifications{opacity:1;top:190px}.notifications{border-radius:2px;-webkit-transition:.25s ease-out .1s opacity;transition:.25s ease-out .1s opacity;background:#ecf0f1;border:1px solid #bdc3c7;left:10px;opacity:0;position:absolute;top:-999px}.notifications:after{border:10px solid transparent;border-bottom-color:#3498db;content:'';display:block;height:0;position:absolute;top:-20px;width:0}.notifications .show-all,.notifications h3{background:#3498db;color:#fefefe;margin:0;padding:10px;width:350px}.notifications h3{cursor:default;font-size:1.05em;font-weight:400}.notifications .show-all{display:block;text-align:center;text-decoration:none}.notifications .show-all:focus,.notifications .show-all:hover{text-decoration:underline}.notifications .notifications-list{list-style:none;margin:0;overflow:hidden;padding:0}.notifications .notifications-list .item{-webkit-transition:-webkit-transform .25s ease-out .1s;transition:transform .25s ease-out .1s;border-top:1px solid #bdc3c7;color:#7f8c8d;cursor:default;display:block;padding:10px;position:relative;white-space:nowrap;width:350px}.notifications .notifications-list .item .button-dismiss,.notifications .notifications-list .item .details,.notifications .notifications-list .item:before{display:inline-block;vertical-align:middle}.notifications .notifications-list .item:before{border-radius:50%;background:#3498db;content:'';height:8px;width:8px}.notifications .notifications-list .item .details{margin-left:10px;white-space:normal;width:280px}.notifications .notifications-list .item .details .date,.notifications .notifications-list .item .details .title{display:block}.notifications .notifications-list .item .details .date{color:#95a5a6;font-size:.85em;margin-top:3px}.notifications .notifications-list .item .button-dismiss{color:#bdc3c7;font-size:2.25em}.notifications .notifications-list .item .button-dismiss:focus,.notifications .notifications-list .item .button-dismiss:hover{color:#95a5a6}.notifications .notifications-list .item.expired,.notifications .notifications-list .item.expired .details .date{color:#bdc3c7}.notifications .notifications-list .item.no-data{display:none;text-align:center}.notifications .notifications-list .item.no-data:before{display:none}.notifications .notifications-list .item.expired:before{background:#bdc3c7}.notifications .notifications-list .item.dismissed{-webkit-transform:translateX(100%);-ms-transform:translateX(100%);transform:translateX(100%)}.notifications.empty .notifications-list .no-data{display:block;padding:10px}.ngsb-wrap{-ms-touch-action:none}.ngsb-wrap .ngsb-container{width:auto;overflow:hidden;-webkit-transition:.5s all;transition:.5s all}.ngsb-wrap:hover .ngsb-scrollbar{opacity:1;filter:"alpha(opacity=100)";-ms-filter:"alpha(opacity=100)"}.ngsb-wrap .ngsb-scrollbar{width:16px;height:100%;top:0;right:0;opacity:.75;filter:"alpha(opacity=75)";-ms-filter:"alpha(opacity=75)"}.ngsb-wrap .ngsb-scrollbar .ngsb-thumb-container{position:absolute;top:0;left:0;bottom:0;right:0;height:auto}.ngsb-wrap .ngsb-scrollbar a.ngsb-thumb-container{margin:20px 0}.ngsb-wrap .ngsb-scrollbar .ngsb-track{height:100%;margin:0 auto;width:6px;background:#000;background:rgba(0,0,0,.4);border-radius:2px;filter:"alpha(opacity=40)";-ms-filter:"alpha(opacity=40)";box-shadow:1px 1px 1px rgba(255,255,255,.1)}.ngsb-wrap .ngsb-scrollbar .ngsb-thumb-pos{cursor:pointer;width:100%;height:30px}.ngsb-wrap .ngsb-scrollbar .ngsb-thumb-pos .ngsb-thumb{-webkit-transition:.5s all;transition:.5s all;width:4px;height:100%;margin:0 auto;border-radius:10px;text-align:center;background:#fff;background:rgba(255,255,255,.4);filter:"alpha(opacity=40)";-ms-filter:"alpha(opacity=40)"}.ngsb-wrap .ngsb-scrollbar .ngsb-thumb-pos:hover .ngsb-thumb{background:rgba(255,255,255,.5);filter:"alpha(opacity=50)";-ms-filter:"alpha(opacity=50)"}.ngsb-wrap .ngsb-scrollbar .ngsb-thumb-pos:active{background:rgba(255,255,255,.6);filter:"alpha(opacity=60)";-ms-filter:"alpha(opacity=60)"}.ng-cloak,[ng-cloak],[ng\:cloak]{display:none!important} -------------------------------------------------------------------------------- /Resources/public/js/notification/external/ng-scrollbar-fix.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | angular.module('ngScrollbar', []).directive('ngScrollbar', [ 3 | '$parse', 4 | '$window', 5 | function ($parse, $window) { 6 | return { 7 | restrict: 'A', 8 | replace: true, 9 | transclude: true, 10 | scope: { 'showYScrollbar': '=?isBarShown' }, 11 | link: function (scope, element, attrs) { 12 | var mainElm, transculdedContainer, tools, thumb, thumbLine, track; 13 | var flags = { bottom: attrs.hasOwnProperty('bottom') }; 14 | var win = angular.element($window); 15 | // Elements 16 | var dragger = { top: 0 }, page = { top: 0 }; 17 | // Styles 18 | var scrollboxStyle, draggerStyle, draggerLineStyle, pageStyle; 19 | var calcStyles = function () { 20 | scrollboxStyle = { 21 | position: 'relative', 22 | overflow: 'hidden', 23 | 'max-width': '100%', 24 | height: '100%' 25 | }; 26 | if (page.height) { 27 | scrollboxStyle.height = 100 + page.height + 'px'; 28 | } 29 | draggerStyle = { 30 | position: 'absolute', 31 | height: dragger.height + 'px', 32 | top: dragger.top + 'px' 33 | }; 34 | draggerLineStyle = { 35 | position: 'relative', 36 | 'line-height': dragger.height + 'px' 37 | }; 38 | pageStyle = { 39 | position: 'relative', 40 | top: page.top + 'px', 41 | overflow: 'hidden' 42 | }; 43 | }; 44 | var redraw = function () { 45 | thumb.css('top', dragger.top + 'px'); 46 | var draggerOffset = dragger.top / page.height; 47 | page.top = -Math.round(page.scrollHeight * draggerOffset); 48 | transculdedContainer.css('top', page.top + 'px'); 49 | }; 50 | var trackClick = function (event) { 51 | var offsetY = event.hasOwnProperty('offsetY') ? event.offsetY : event.layerY; 52 | var newTop = Math.max(0, Math.min(parseInt(dragger.trackHeight, 10) - parseInt(dragger.height, 10), offsetY)); 53 | dragger.top = newTop; 54 | redraw(); 55 | event.stopPropagation(); 56 | }; 57 | var wheelHandler = function (event) { 58 | var wheelDivider = 20; 59 | // so it can be changed easily 60 | var deltaY = event.wheelDeltaY !== undefined ? event.wheelDeltaY / wheelDivider : event.wheelDelta !== undefined ? event.wheelDelta / wheelDivider : -event.detail * (wheelDivider / 10); 61 | dragger.top = Math.max(0, Math.min(parseInt(page.height, 10) - parseInt(dragger.height, 10), parseInt(dragger.top, 10) - deltaY)); 62 | redraw(); 63 | if (!!event.preventDefault) { 64 | event.preventDefault(); 65 | } else { 66 | return false; 67 | } 68 | }; 69 | var lastOffsetY = 0; 70 | var thumbDrag = function (event, offsetX, offsetY) { 71 | dragger.top = Math.max(0, Math.min(parseInt(dragger.trackHeight, 10) - parseInt(dragger.height, 10), offsetY)); 72 | event.stopPropagation(); 73 | }; 74 | var dragHandler = function (event) { 75 | var newOffsetX = 0; 76 | var newOffsetY = event.pageY - thumb[0].scrollTop - lastOffsetY; 77 | thumbDrag(event, newOffsetX, newOffsetY); 78 | redraw(); 79 | }; 80 | var _mouseUp = function (event) { 81 | win.off('mousemove', dragHandler); 82 | win.off('mouseup', _mouseUp); 83 | event.stopPropagation(); 84 | }; 85 | var _touchDragHandler = function (event) { 86 | var newOffsetX = 0; 87 | var newOffsetY = event.originalEvent.changedTouches[0].pageY - thumb[0].scrollTop - lastOffsetY; 88 | thumbDrag(event, newOffsetX, newOffsetY); 89 | redraw(); 90 | }; 91 | var _touchEnd = function (event) { 92 | win.off('touchmove', _touchDragHandler); 93 | win.off('touchend', _touchEnd); 94 | event.stopPropagation(); 95 | }; 96 | var buildScrollbar = function (rollToBottom) { 97 | // Getting top position of a parent element to place scroll correctly 98 | var parentOffsetTop = element[0].parentElement.offsetTop; 99 | var wheelEvent = win[0].onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll'; 100 | rollToBottom = flags.bottom || rollToBottom; 101 | mainElm = angular.element(element.children()[0]); 102 | transculdedContainer = angular.element(mainElm.children()[0]); 103 | tools = angular.element(mainElm.children()[1]); 104 | thumb = angular.element(angular.element(tools.children()[0]).children()[0]); 105 | thumbLine = angular.element(thumb.children()[0]); 106 | track = angular.element(angular.element(tools.children()[0]).children()[1]); 107 | // Check if scroll bar is needed 108 | page.height = element[0].offsetHeight; 109 | page.scrollHeight = transculdedContainer[0].scrollHeight; 110 | if (page.height < page.scrollHeight) { 111 | scope.showYScrollbar = true; 112 | scope.$emit('scrollbar.show'); 113 | // Calculate the dragger height 114 | dragger.height = Math.round(page.height / page.scrollHeight * page.height); 115 | dragger.trackHeight = page.height; 116 | // update the transcluded content style and clear the parent's 117 | calcStyles(); 118 | element.css({ overflow: 'hidden' }); 119 | mainElm.css(scrollboxStyle); 120 | transculdedContainer.css(pageStyle); 121 | thumb.css(draggerStyle); 122 | thumbLine.css(draggerLineStyle); 123 | // Bind scroll bar events 124 | track.bind('click', trackClick); 125 | // Handl mousewheel 126 | transculdedContainer[0].addEventListener(wheelEvent, wheelHandler, false); 127 | // Drag the scroller with the mouse 128 | thumb.on('mousedown', function (event) { 129 | lastOffsetY = event.pageY - thumb[0].offsetTop; 130 | win.on('mouseup', _mouseUp); 131 | win.on('mousemove', dragHandler); 132 | event.preventDefault(); 133 | }); 134 | // Drag the scroller by touch 135 | thumb.on('touchstart', function (event) { 136 | lastOffsetY = event.originalEvent.changedTouches[0].pageY - thumb[0].offsetTop; 137 | win.on('touchend', _touchEnd); 138 | win.on('touchmove', _touchDragHandler); 139 | event.preventDefault(); 140 | }); 141 | if (rollToBottom) { 142 | flags.bottom = false; 143 | dragger.top = parseInt(page.height, 10) - parseInt(dragger.height, 10); 144 | } else { 145 | dragger.top = Math.max(0, Math.min(parseInt(page.height, 10) - parseInt(dragger.height, 10), parseInt(dragger.top, 10))); 146 | } 147 | redraw(); 148 | } else { 149 | scope.showYScrollbar = false; 150 | scope.$emit('scrollbar.hide'); 151 | thumb.off('mousedown'); 152 | transculdedContainer[0].removeEventListener(wheelEvent, wheelHandler, false); 153 | transculdedContainer.attr('style', 'position:relative;top:0'); 154 | // little hack to remove other inline styles 155 | mainElm.css({ height: '100%' }); 156 | } 157 | }; 158 | var rebuildTimer; 159 | var rebuild = function (e, data) { 160 | /* jshint -W116 */ 161 | if (rebuildTimer != null) { 162 | clearTimeout(rebuildTimer); 163 | } 164 | /* jshint +W116 */ 165 | var rollToBottom = !!data && !!data.rollToBottom; 166 | rebuildTimer = setTimeout(function () { 167 | page.height = null; 168 | buildScrollbar(rollToBottom); 169 | if (!scope.$$phase) { 170 | scope.$digest(); 171 | } 172 | // update parent for flag update 173 | if (!scope.$parent.$$phase) { 174 | scope.$parent.$digest(); 175 | } 176 | }, 72); 177 | }; 178 | buildScrollbar(); 179 | if (!!attrs.rebuildOn) { 180 | attrs.rebuildOn.split(' ').forEach(function (eventName) { 181 | scope.$on(eventName, rebuild); 182 | }); 183 | } 184 | if (attrs.hasOwnProperty('rebuildOnResize')) { 185 | win.on('resize', rebuild); 186 | } 187 | }, 188 | template: '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' 189 | }; 190 | } 191 | ]); -------------------------------------------------------------------------------- /Resources/public/js/notification/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var uglify = require('gulp-uglify'); 3 | var minifyCSS = require('gulp-minify-css'); 4 | var less = require('gulp-less'); 5 | var watch = require('gulp-watch'); 6 | var browserSync = require('browser-sync'); 7 | var autoprefix = require('gulp-autoprefixer'); 8 | var coffee = require('gulp-coffee'); 9 | var browserify = require('browserify'); 10 | var source = require('vinyl-source-stream'); 11 | var buffer = require('vinyl-buffer'); 12 | var gutil = require('gulp-util'); 13 | var sourcemaps = require('gulp-sourcemaps'); 14 | var assign = require('lodash.assign'); 15 | var watchify = require('watchify'); 16 | var sequence = require('gulp-watch-sequence'); 17 | var argv = require('yargs').argv; 18 | var gulpif = require('gulp-if'); 19 | var coffeeify = require('coffeeify'); 20 | var ngHtml2Js = require('browserify-ng-html2js'); 21 | 22 | const OUTPUT_FILE_NAME = 'gos-notification.min.js'; 23 | const ENTRY_FILE = './src/gos-notification.coffee'; 24 | const DIST_DIR = './dist'; 25 | const STYLE_DIR = './src/style'; 26 | 27 | var bundler = browserify({ 28 | entries: [ENTRY_FILE], 29 | extensions: ['.coffee'], 30 | debug: !argv.production, 31 | cache: {}, 32 | packageCache: {}, 33 | fullPaths: true 34 | }); 35 | 36 | var bundle = function(){ 37 | var handler = bundler.bundle() 38 | .on('error', gutil.log.bind(gutil, 'Browserify Error')) 39 | .pipe(source(OUTPUT_FILE_NAME)) 40 | .pipe(buffer()) 41 | .pipe(gulpif(!argv.production, sourcemaps.init({loadMaps: true}))) 42 | .pipe(gulpif(!argv.production, sourcemaps.write('./'))) 43 | .pipe(gulpif(argv.production, uglify())) 44 | .pipe(gulp.dest(DIST_DIR)); 45 | 46 | gutil.log("Updated JavaScript sources"); 47 | 48 | return handler; 49 | }; 50 | 51 | gulp.task('less', function() { 52 | gulp.src(STYLE_DIR + '/*.less') 53 | .pipe(less()) 54 | .pipe(autoprefix('last 2 version', 'ie 8', 'ie 9')) 55 | .pipe(minifyCSS({ 56 | 'comments' : true, 57 | 'spare': true 58 | })) 59 | .pipe(gulp.dest(DIST_DIR)) 60 | }); 61 | 62 | gulp.task('browserify', function (){ 63 | return bundle(); 64 | }); 65 | 66 | gulp.task('watch', function(){ 67 | var watcher = watchify(bundler); 68 | watcher.on('update', bundle, true); 69 | 70 | bundle(); 71 | 72 | gulp.watch(STYLE_DIR + '/*.less', function(){ 73 | gulp.run('less'); 74 | gutil.log('Update css'); 75 | }); 76 | }); 77 | 78 | gulp.task('default', ['watch']); 79 | gulp.task('serve', ['browserify', 'less']); 80 | -------------------------------------------------------------------------------- /Resources/public/js/notification/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GosNotification", 3 | "version": "0.1.0", 4 | "description": "GosNotification Client Side of GosNotificationBundle", 5 | "main": "./src/gos-notification.js", 6 | "author": "Johann Saunier ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "angular": "^1.3.15", 10 | "angular-animate": "^1.3.15", 11 | "angular-moment": "^0.10.0", 12 | "angular-toastr": "^1.2.1", 13 | "ng-scrollbar": "0.0.7" 14 | }, 15 | "devDependencies": { 16 | "browser-sync": "^2.6.5", 17 | "browserify": "^9.0.8", 18 | "browserify-ng-html2js": "^1.0.1", 19 | "browserify-shim": "^3.8.5", 20 | "coffeeify": "^1.0.0", 21 | "gulp": "^3.8.11", 22 | "gulp-autoprefixer": "^2.2.0", 23 | "gulp-browserify": "^0.5.1", 24 | "gulp-coffee": "^2.3.1", 25 | "gulp-coffeeify": "^0.1.8", 26 | "gulp-concat": "^2.5.2", 27 | "gulp-filter": "^2.0.2", 28 | "gulp-if": "^1.2.5", 29 | "gulp-less": "^3.0.3", 30 | "gulp-minify-css": "^1.1.0", 31 | "gulp-sourcemaps": "^1.5.2", 32 | "gulp-uglify": "^1.2.0", 33 | "gulp-util": "^3.0.4", 34 | "gulp-watch": "^4.2.4", 35 | "gulp-watch-sequence": "0.0.4", 36 | "lodash.assign": "^3.1.0", 37 | "vinyl-buffer": "^1.0.0", 38 | "vinyl-source-stream": "^1.1.0", 39 | "watchify": "^3.1.0", 40 | "yargs": "^3.8.0" 41 | }, 42 | "browser": { 43 | "angular-toastr": "./node_modules/angular-toastr/dist/angular-toastr.js", 44 | "angular-scrollbar": "./external/ng-scrollbar-fix.js" 45 | }, 46 | "browserify": { 47 | "transform": [ 48 | "browserify-shim", 49 | "coffeeify" 50 | ] 51 | }, 52 | "browserify-shim": { 53 | "angular-toastr": { 54 | "exports": "angular.module('toastr').name" 55 | }, 56 | "angular-scrollbar": { 57 | "exports": "angular.module('ngScrollbar').name" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Resources/public/js/notification/src/controller/BoardController.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = ['$rootScope', '$scope', 'BoardService', ($rootScope, $scope, BoardService) -> 4 | $scope.display = false 5 | $scope.notifications = [] 6 | 7 | $scope.dismiss = (notification) -> 8 | BoardService.dismiss notification 9 | 10 | $scope.$on 'board:display', (event, arg) -> 11 | $scope.display = arg 12 | 13 | if !$scope.display 14 | return 15 | 16 | BoardService.load $scope 17 | return 18 | return 19 | ] -------------------------------------------------------------------------------- /Resources/public/js/notification/src/controller/RealtimeController.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = ['$scope', 'toastr', ($scope, toastr) -> 4 | 5 | $scope.$on 'notification:new', (event, args) -> 6 | notification = args.notification 7 | notifConfig = 8 | allowHtml: true 9 | timeOut: notification.timeout 10 | 11 | toastr[notification.type] notification.content, notification.title, notifConfig 12 | 13 | return 14 | return 15 | ] -------------------------------------------------------------------------------- /Resources/public/js/notification/src/controller/ToggleController.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = ['$scope', '$rootScope', ($scope, $rootScope) -> 4 | $scope.open = false 5 | 6 | $scope.toggleNotification = -> 7 | $scope.open = !$scope.open 8 | $rootScope.$broadcast 'board:display', $scope.open 9 | return 10 | return 11 | ] 12 | -------------------------------------------------------------------------------- /Resources/public/js/notification/src/directive/GosNotification.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = ['$scope', ($scope) -> 4 | return { 5 | # templateUrl: 'views/notification-list.html', 6 | template: 'fuck you', 7 | restrict: 'EA', 8 | replace: true 9 | } 10 | ] -------------------------------------------------------------------------------- /Resources/public/js/notification/src/gos-notification.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | angular = require('angular') 4 | 5 | notificationApp = angular.module('notificationApp', [ 6 | require('angular-toastr') 7 | require('angular-moment') 8 | require('angular-scrollbar') 9 | ]) 10 | 11 | notificationApp.constant 'Version', require('../package.json').version 12 | notificationApp.constant 'appConfigs', notificationConfig 13 | 14 | notificationApp.config [ '$interpolateProvider', '$sceProvider', '$httpProvider', ($interpolateProvider, $sceProvider, $httpProvider) -> 15 | $sceProvider.enabled false 16 | $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' 17 | $interpolateProvider.startSymbol('[[').endSymbol(']]') 18 | return 19 | ] 20 | 21 | notificationApp.run ['WebsocketService', (WebsocketService) -> 22 | WebsocketService.connect() 23 | return 24 | ] 25 | 26 | notificationApp.directive 'notificationDirective', require('./directive/GosNotification') 27 | notificationApp.service 'WebsocketService', require('./service/WebsocketService') 28 | notificationApp.service 'NotificationService', require('./service/NotificationService') 29 | notificationApp.service 'BoardService', require('./service/BoardService') 30 | notificationApp.controller 'ToggleController', require('./controller/ToggleController') 31 | notificationApp.controller 'RealtimeController', require('./controller/RealtimeController') 32 | notificationApp.controller 'BoardController', require('./controller/BoardController') 33 | -------------------------------------------------------------------------------- /Resources/public/js/notification/src/service/BoardService.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = ['$rootScope', 'NotificationService', 'appConfigs', '$q', ($rootScope, NotificationService, appConfigs, $q) -> 4 | @start = 1 5 | @end = 15 6 | 7 | @update = -> 8 | return 9 | 10 | @dismiss = (notification) -> 11 | NotificationService.markAsViewed(notification.channel, notification.uuid, () -> 12 | if appConfigs.debug 13 | console.log('Marked as read : '+ notification.channel+':'+notification.uuid) 14 | ) 15 | return 16 | 17 | @notificationCallback = ($scope, channel, eventName) -> 18 | self = @ 19 | 20 | NotificationService.fetch channel, self.start, self.end, (payload) -> 21 | $scope.$apply -> 22 | $scope.notifications = payload.result 23 | $rootScope.$broadcast eventName, $scope.notifications 24 | $rootScope.$broadcast 'notification:board:rebuild' 25 | return 26 | return 27 | return 28 | 29 | @load = ($scope) -> 30 | self = @ 31 | for channel in appConfigs.channels 32 | @notificationCallback $scope, channel, 'notification:board:update' 33 | return 34 | ] -------------------------------------------------------------------------------- /Resources/public/js/notification/src/service/NotificationService.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = ['$rootScope', 'appConfigs', 'WebsocketService', ($rootScope, appConfigs, WebsocketService) -> 4 | 5 | @fetch = (channel, start, end, successCb) -> 6 | self = @ 7 | start = start or 1 8 | end = end or 15 9 | 10 | WebsocketService.session.call('notification/fetch', start: start, end: end, channel: channel) 11 | .then successCb, (error) -> 12 | if appConfigs.debug 13 | console.log error 14 | return 15 | 16 | @markAsViewed = (channel, uuid, successCb) -> 17 | self = @ 18 | 19 | WebsocketService.session.call('notification/markAsViewed', channel: channel, uuid: uuid) 20 | .then successCb, (error) -> 21 | if appConfigs.debug 22 | console.log error 23 | return 24 | 25 | $rootScope.$on 'ws:connect', (event, session) -> 26 | for i of appConfigs.channels 27 | session.subscribe appConfigs.channels[i], (uri, payload) -> 28 | $rootScope.$broadcast 'notification:new', 29 | uri: uri 30 | notification: JSON.parse(payload) 31 | return 32 | return 33 | 34 | return 35 | ] -------------------------------------------------------------------------------- /Resources/public/js/notification/src/service/WebsocketService.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = ['$rootScope', 'appConfigs', ($rootScope, appConfigs) -> 4 | @websocket = null 5 | @connected = false 6 | @hasPreviousConnection = false 7 | @session = null 8 | 9 | @connect = -> 10 | self = @ 11 | 12 | @websocket = WS.connect(appConfigs.websocketURI) 13 | 14 | @websocket.on 'socket/connect', (session) -> 15 | self.connected = true 16 | self.session = session 17 | 18 | if appConfigs.debug 19 | console.log 'connected to ' + appConfigs.websocketURI 20 | 21 | $rootScope.$broadcast 'ws:connect', session 22 | return 23 | 24 | $rootScope.$on 'socket/disconnect', (event, error) -> 25 | if appConfigs.debug 26 | console.log 'Disconnected for ' + error.reason + ' with code ' + error.code 27 | return 28 | 29 | @websocket.on 'socket/disconnect', (error) -> 30 | self.connected = false 31 | self.session = null 32 | self.hasPreviousConnection = true 33 | $rootScope.$broadcast 'ws:disconnect', error 34 | return 35 | return 36 | 37 | @isConnected = -> 38 | @connected 39 | 40 | return 41 | ] -------------------------------------------------------------------------------- /Resources/public/js/notification/src/style/notification-widget.less: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Lato:700); 2 | 3 | *, 4 | *:after, 5 | *:before 6 | { 7 | -moz-box-sizing: border-box; 8 | -webkit-box-sizing: border-box; 9 | box-sizing: border-box; 10 | } 11 | 12 | .button-default 13 | { 14 | .transition(@transitionDefault color); 15 | background: transparent; 16 | border: none; 17 | cursor: pointer; 18 | margin: 0; 19 | outline: none; 20 | position: relative; 21 | } 22 | 23 | .show-notifications 24 | { 25 | position: relative; 26 | 27 | &:hover, 28 | &:focus, 29 | &.active 30 | { 31 | #icon-bell 32 | { 33 | fill: @colorWetAsphalt; 34 | } 35 | } 36 | 37 | #icon-bell 38 | { 39 | fill: @colorAsbestos; 40 | } 41 | 42 | .notifications-count 43 | { 44 | .border-radius(50%); 45 | background: @colorPeterRiver; 46 | color: @colorWhite; 47 | font: normal .85em 'Lato'; 48 | height: 16px; 49 | line-height: 1.75em; 50 | position: absolute; 51 | right: 2px; 52 | text-align: center; 53 | top: -2px; 54 | width: 16px; 55 | } 56 | 57 | &.active ~ .notifications 58 | { 59 | opacity: 1; 60 | top: 190px; 61 | } 62 | } 63 | 64 | .notifications 65 | { 66 | .border-radius(@borderRadius); 67 | .transition(@transitionDefault opacity); 68 | background: @colorClouds; 69 | border: 1px solid @colorSilver; 70 | left: 10px; 71 | opacity: 0; 72 | position: absolute; 73 | top: -999px; 74 | 75 | &:after 76 | { 77 | border: 10px solid transparent; 78 | border-bottom-color: @colorPeterRiver; 79 | content: ''; 80 | display: block; 81 | height: 0; 82 | position: absolute; 83 | top: -20px; 84 | width: 0; 85 | } 86 | 87 | h3, 88 | .show-all 89 | { 90 | background: @colorPeterRiver; 91 | color: @colorWhite; 92 | margin: 0; 93 | padding: 10px; 94 | width: 350px; 95 | } 96 | 97 | h3 98 | { 99 | cursor: default; 100 | font-size: 1.05em; 101 | font-weight: normal; 102 | } 103 | 104 | .show-all 105 | { 106 | display: block; 107 | text-align: center; 108 | text-decoration: none; 109 | 110 | &:hover, 111 | &:focus 112 | { 113 | text-decoration: underline; 114 | } 115 | } 116 | 117 | .notifications-list 118 | { 119 | list-style: none; 120 | margin: 0; 121 | overflow: hidden; 122 | padding: 0; 123 | 124 | .item 125 | { 126 | .transition-transform(@transitionDefault); 127 | border-top: 1px solid @colorSilver; 128 | color: @colorAsbestos; 129 | cursor: default; 130 | display: block; 131 | padding: 10px; 132 | position: relative; 133 | white-space: nowrap; 134 | width: 350px; 135 | 136 | &:before, 137 | .details, 138 | .button-dismiss 139 | { 140 | display: inline-block; 141 | vertical-align: middle; 142 | } 143 | 144 | &:before 145 | { 146 | .border-radius(50%); 147 | background: @colorPeterRiver; 148 | content: ''; 149 | height: 8px; 150 | width: 8px; 151 | } 152 | 153 | .details 154 | { 155 | margin-left: 10px; 156 | white-space: normal; 157 | width: 280px; 158 | 159 | .title, 160 | .date 161 | { 162 | display: block; 163 | } 164 | 165 | .date 166 | { 167 | color: @colorConcrete; 168 | font-size: .85em; 169 | margin-top: 3px; 170 | } 171 | } 172 | 173 | .button-dismiss 174 | { 175 | color: @colorSilver; 176 | font-size: 2.25em; 177 | 178 | &:hover, 179 | &:focus 180 | { 181 | color: @colorConcrete; 182 | } 183 | } 184 | 185 | &.no-data 186 | { 187 | display: none; 188 | text-align: center; 189 | 190 | &:before 191 | { 192 | display: none; 193 | } 194 | } 195 | 196 | &.expired 197 | { 198 | color: @colorSilver; 199 | 200 | &:before 201 | { 202 | background: @colorSilver; 203 | } 204 | 205 | .details 206 | { 207 | .date 208 | { 209 | color: @colorSilver; 210 | } 211 | } 212 | } 213 | 214 | &.dismissed 215 | { 216 | .transform(translateX(100%)); 217 | } 218 | } 219 | } 220 | 221 | &.empty 222 | { 223 | .notifications-list 224 | { 225 | .no-data 226 | { 227 | display: block; 228 | padding: 10px; 229 | } 230 | } 231 | } 232 | } 233 | 234 | /* variables */ 235 | @colorClouds: #ecf0f1; 236 | @colorSilver: #bdc3c7; 237 | @colorWhite: #fefefe; 238 | @colorPeterRiver: #3498db; 239 | @colorConcrete: #95a5a6; 240 | @colorAsbestos: #7f8c8d; 241 | @colorWetAsphalt: #34495e; 242 | 243 | @borderRadius: 2px; 244 | 245 | @transitionDefault: 0.25s ease-out 0.10s; 246 | 247 | /* mixins */ 248 | .background-clip(@value: border-box) 249 | { 250 | -moz-background-clip: @value; 251 | -webkit-background-clip: @value; 252 | background-clip: @value; 253 | } 254 | 255 | .border-radius(@value: 5px) 256 | { 257 | -moz-border-radius: @value; 258 | -webkit-border-radius: @value; 259 | border-radius: @value; 260 | .background-clip(padding-box); 261 | } 262 | 263 | .transform(@value) 264 | { 265 | -webkit-transform: @value; 266 | -moz-transform: @value; 267 | -ms-transform: @value; 268 | -o-transform: @value; 269 | transform: @value; 270 | } 271 | 272 | .transition(@value: all 0.25s ease-out) 273 | { 274 | -webkit-transition: @value; 275 | -moz-transition: @value; 276 | -o-transition: @value; 277 | transition: @value; 278 | } 279 | 280 | .transition-transform(@transition: 0.25s ease-out) 281 | { 282 | -webkit-transition: -webkit-transform @transition; 283 | -moz-transition: -moz-transform @transition; 284 | -o-transition: -o-transform @transition; 285 | transition: transform @transition; 286 | } 287 | 288 | .ngsb-wrap { 289 | -ms-touch-action:none; 290 | 291 | .ngsb-container{ 292 | width:auto; 293 | overflow:hidden; 294 | transition: 0.5s all; 295 | } 296 | 297 | &:hover { 298 | .ngsb-scrollbar { 299 | opacity:1; 300 | filter:"alpha(opacity=100)"; -ms-filter:"alpha(opacity=100)"; /* old ie */ 301 | } 302 | } 303 | 304 | .ngsb-scrollbar { 305 | width:16px; 306 | height:100%; 307 | top:0; 308 | right:0; 309 | opacity:0.75; 310 | filter:"alpha(opacity=75)"; -ms-filter:"alpha(opacity=75)"; /* old ie */ 311 | 312 | .ngsb-thumb-container{ 313 | position:absolute; 314 | top:0; 315 | left:0; 316 | bottom:0; 317 | right:0; 318 | height:auto; 319 | } 320 | 321 | a+ { 322 | &.ngsb-thumb-container{ 323 | margin:20px 0; 324 | } 325 | } 326 | 327 | .ngsb-track{ 328 | height:100%; 329 | margin:0 auto; 330 | width:6px; 331 | background: #000; 332 | background: rgba(0, 0, 0, 0.4); 333 | -webkit-border-radius:2px; 334 | -moz-border-radius:2px; 335 | border-radius:2px; 336 | filter:"alpha(opacity=40)"; -ms-filter:"alpha(opacity=40)"; /* old ie */ 337 | box-shadow:1px 1px 1px rgba(255,255,255,0.1); 338 | } 339 | 340 | .ngsb-thumb-pos { 341 | cursor:pointer; 342 | width:100%; 343 | height:30px; 344 | 345 | .ngsb-thumb { 346 | transition: 0.5s all; 347 | width:4px; 348 | height:100%; 349 | margin:0 auto; 350 | -webkit-border-radius:10px; 351 | -moz-border-radius:10px; 352 | border-radius:10px; 353 | text-align:center; 354 | background:#fff; /* rgba fallback */ 355 | background:rgba(255,255,255,0.4); 356 | filter:"alpha(opacity=40)"; 357 | -ms-filter:"alpha(opacity=40)"; /* old ie */ 358 | } 359 | 360 | &:hover { 361 | .ngsb-thumb { 362 | background:rgba(255,255,255,0.5); 363 | filter:"alpha(opacity=50)"; 364 | -ms-filter:"alpha(opacity=50)"; /* old ie */ 365 | } 366 | } 367 | 368 | &:active { 369 | background:rgba(255,255,255,0.6); 370 | filter:"alpha(opacity=60)"; -ms-filter:"alpha(opacity=60)"; /* old ie */ 371 | } 372 | } 373 | } 374 | } 375 | 376 | [ng\:cloak], [ng-cloak], .ng-cloak { 377 | display: none !important; 378 | } -------------------------------------------------------------------------------- /Resources/public/js/notification/src/views/notification-list.html: -------------------------------------------------------------------------------- 1 |
2 | [[ notification.title ]] 3 | [[ notification.created_at | date ]] 4 |
5 | 6 | -------------------------------------------------------------------------------- /Router/Dumper/RedisDumper.php: -------------------------------------------------------------------------------- 1 | router = $generator; 39 | $this->tokenizer = $tokenizer; 40 | $this->routeCollection = $routeCollection; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function dump($tokenSeparator = ':', Array $extras = []) 47 | { 48 | $subscriptions = [ 49 | self::SUBSCRIBE => [], 50 | self::PSUBSCRIBE => [], 51 | ]; 52 | 53 | /** @var RouteInterface $route */ 54 | foreach ($this->routeCollection as $routeName => $route) { 55 | $route->setName($routeName); 56 | $tokens = $this->tokenizer->tokenize($route, $tokenSeparator); 57 | 58 | if (false === $tokens) { 59 | continue; 60 | } 61 | 62 | $attributeCount = 0; 63 | $wildcardAttribute = 0; 64 | $routeParameters = []; 65 | 66 | foreach ($tokens as $token) { 67 | if ($token->isParameter()) { 68 | ++$attributeCount; 69 | 70 | $requirements = $token->getRequirements(); 71 | 72 | if (isset($requirements['wildcard']) && true === $requirements['wildcard']) { 73 | ++$wildcardAttribute; 74 | 75 | $routeParameters[$token->getExpression()] = '*'; 76 | } 77 | } 78 | } 79 | 80 | if ($attributeCount === $wildcardAttribute && $wildcardAttribute > 0) { 81 | $subscriptions[self::PSUBSCRIBE][] = $this->router->generate( 82 | $routeName, 83 | $routeParameters, 84 | $tokenSeparator 85 | ); 86 | } 87 | } 88 | 89 | foreach ($extras as $extra) { 90 | list($routeName, $routeParameters) = $extra; 91 | 92 | $routeParameters = array_map(function ($value) { 93 | if ($value === 'all') { 94 | return '*'; 95 | } 96 | }, $routeParameters); 97 | 98 | $channel = $this->router->generate($routeName, $routeParameters, $tokenSeparator); 99 | 100 | if (false === strpos($channel, '*')) { 101 | $subscriptions[self::SUBSCRIBE][] = $channel; 102 | } else { 103 | $subscriptions[self::PSUBSCRIBE][] = $channel; 104 | } 105 | } 106 | 107 | return $subscriptions; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Serializer/NotificationContextSerializer.php: -------------------------------------------------------------------------------- 1 | notificationContextClass = $notificationContextClass; 40 | 41 | $normalizer = new GetSetMethodNormalizer(); 42 | 43 | $this->normalizers = array($normalizer); 44 | $this->encoders = array(new JsonEncoder()); 45 | 46 | $this->serializer = new Serializer($this->normalizers, $this->encoders); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function serialize(NotificationContextInterface $context) 53 | { 54 | return $this->serializer->serialize($context, 'json'); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function deserialize($message) 61 | { 62 | return $this->serializer->deserialize($message, $this->notificationContextClass, 'json'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Serializer/NotificationContextSerializerInterface.php: -------------------------------------------------------------------------------- 1 | notificationClass = $notificationClass; 40 | 41 | $dateCallback = function ($datetime) { 42 | return $datetime instanceof \DateTime 43 | ? $datetime->format(\DateTime::W3C) 44 | : null; 45 | }; 46 | 47 | $normalizer = new GetSetMethodNormalizer(); 48 | 49 | $normalizer->setCallbacks(array( 50 | 'createdAt' => $dateCallback, 51 | 'viewedAt' => $dateCallback, 52 | )); 53 | 54 | $this->normalizers = array($normalizer); 55 | $this->encoders = array(new JsonEncoder()); 56 | 57 | $this->serializer = new Serializer($this->normalizers, $this->encoders); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function serialize(NotificationInterface $notification) 64 | { 65 | return $this->serializer->serialize($notification, 'json'); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function deserialize($message) 72 | { 73 | return $this->serializer->deserialize($message, $this->notificationClass, 'json'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Serializer/NotificationSerializerInterface.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class PubSubServer implements ServerInterface 26 | { 27 | /** @var LoopInterface */ 28 | protected $loop; 29 | 30 | /** @var Client */ 31 | protected $client; 32 | 33 | /** @var LoggerInterface */ 34 | protected $logger; 35 | 36 | /** @var EventDispatcherInterface */ 37 | protected $eventDispatcher; 38 | 39 | /** @var RedisDumper */ 40 | protected $redisDumper; 41 | 42 | /** @var PubSubContext */ 43 | protected $pubSub; 44 | 45 | /** @var ServerNotificationProcessorInterface */ 46 | protected $processor; 47 | 48 | /** @var bool */ 49 | protected $debug; 50 | 51 | /** 52 | * @param EventDispatcherInterface $eventDispatcher 53 | * @param array $pubSubConfig 54 | * @param RedisDumper $redisDumper 55 | * @param ServerNotificationProcessorInterface $processor 56 | * @param bool $debug 57 | * @param LoggerInterface $logger 58 | */ 59 | public function __construct( 60 | EventDispatcherInterface $eventDispatcher, 61 | RedisDumper $redisDumper, 62 | ServerNotificationProcessorInterface $processor, 63 | $debug, 64 | LoggerInterface $logger = null 65 | ) { 66 | $this->eventDispatcher = $eventDispatcher; 67 | $this->redisDumper = $redisDumper; 68 | $this->processor = $processor; 69 | $this->debug = $debug; 70 | $this->logger = null === $logger ? new NullLogger() : $logger; 71 | } 72 | 73 | /** 74 | * @return array 75 | */ 76 | protected function getSubscriptions() 77 | { 78 | $subscription = $this->redisDumper->dump(); 79 | 80 | if (!empty($subscription['subscribe'])) { 81 | $this->logger->info(sprintf( 82 | 'Listening topics %s', 83 | implode(', ', $subscription['subscribe']) 84 | )); 85 | } 86 | 87 | if (!empty($subscription['psubscribe'])) { 88 | $this->logger->info(sprintf( 89 | 'Listening pattern %s', 90 | implode(', ', $subscription['psubscribe']) 91 | )); 92 | } 93 | 94 | return $subscription; 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public function launch($host, $port, $profile) 101 | { 102 | $this->logger->info('Starting redis pubsub'); 103 | 104 | $this->loop = Factory::create(); 105 | 106 | if (extension_loaded('pcntl')) { 107 | $this->handlePnctlEvent(); 108 | } 109 | 110 | $this->client = new Client('tcp://' . $host . ':' . $port, $this->loop); 111 | 112 | $dispatcher = new EventEmitter(); 113 | $dispatcher->on('notification', $this->processor); 114 | 115 | if (true === $profile) { 116 | $this->loop->addPeriodicTimer(5, function () { 117 | $this->logger->info('Memory usage : ' . round((memory_get_usage() / (1024 * 1024)), 4) . 'Mo'); 118 | }); 119 | } 120 | 121 | $subscriptions = $this->getSubscriptions(); 122 | 123 | 124 | $this->client->connect(function ($client) use ($dispatcher, $subscriptions) { 125 | 126 | $this->pubSub = $client->pubSubLoop($subscriptions, function ($event, $pubsub) use ($dispatcher) { 127 | if ($event->payload === 'quit') { 128 | $this->stop(); 129 | } 130 | 131 | if (!in_array($event->kind, array(Consumer::MESSAGE, Consumer::PMESSAGE))) { 132 | throw new NotificationServerException(sprintf( 133 | 'Unsupported message type %s given, supported [%]', 134 | $event->kind, 135 | [Consumer::MESSAGE, Consumer::PMESSAGE, Consumer::PSUBSCRIBE, Consumer::SUBSCRIBE, Consumer::UNSUBSCRIBE] 136 | )); 137 | } 138 | 139 | if (in_array($event->kind, [Consumer::MESSAGE, Consumer::PMESSAGE])) { 140 | if ($event->kind === Consumer::MESSAGE) { 141 | $message = new Message( 142 | $event->kind, 143 | $event->channel, 144 | $event->payload 145 | ); 146 | } 147 | 148 | if ($event->kind === Consumer::PMESSAGE) { 149 | $message = new PatternMessage( 150 | $event->kind, 151 | $event->pattern, 152 | $event->channel, 153 | $event->payload 154 | ); 155 | } 156 | 157 | $dispatcher->emit('notification', [$message]); 158 | } 159 | }); 160 | }); 161 | 162 | $this->logger->info(sprintf( 163 | 'Launching %s on %s', 164 | $this->getName(), 165 | $host . ':' . $port 166 | )); 167 | 168 | $this->loop->run(); 169 | } 170 | 171 | protected function stop() 172 | { 173 | if (null !== $this->pubSub) { 174 | $this->pubSub->quit(); 175 | } 176 | 177 | $this->client->getConnection()->disconnect(); 178 | $this->loop->stop(); 179 | } 180 | 181 | protected function handlePnctlEvent() 182 | { 183 | $pnctlEmitter = new PnctlEmitter($this->loop); 184 | 185 | $pnctlEmitter->on(SIGTERM, function () { 186 | $this->logger->notice('Stopping server ...'); 187 | $this->stop(); 188 | $this->logger->notice('Server stopped !'); 189 | }); 190 | 191 | $pnctlEmitter->on(SIGINT, function () { 192 | $this->logger->notice('Press CTLR+C again to stop the server'); 193 | 194 | if (SIGINT === pcntl_sigtimedwait([SIGINT], $siginfo, 5)) { 195 | $this->logger->notice('Stopping server ...'); 196 | $this->stop(); 197 | $this->logger->notice('Server stopped !'); 198 | } else { 199 | $this->logger->notice('CTLR+C not pressed, continue to run normally'); 200 | } 201 | }); 202 | } 203 | 204 | /** 205 | * {@inheritdoc} 206 | */ 207 | public function getName() 208 | { 209 | return 'PubSub'; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Server/ServerNotificationProcessor.php: -------------------------------------------------------------------------------- 1 | notificationSerializer = $notificationSerializer; 73 | $this->contextSerializer = $contextSerializer; 74 | $this->matcher = $matcher; 75 | $this->pusherRegistry = $pusherRegistry; 76 | $this->container = $container; 77 | $this->eventDispatcher = $eventDispatcher; 78 | $this->logger = null === $logger ? new NullLogger() : $logger; 79 | } 80 | 81 | /** 82 | * @param LoopInterface $loop 83 | */ 84 | public function setLoop(LoopInterface $loop) 85 | { 86 | $this->loop = $loop; 87 | } 88 | 89 | /** 90 | * @param MessageInterface $message 91 | * 92 | * @return array 93 | */ 94 | public function getNotification(MessageInterface $message) 95 | { 96 | $decodedMessage = json_decode($message->getPayload(), true); 97 | 98 | $notification = $this->notificationSerializer->deserialize(json_encode($decodedMessage['notification'])); 99 | 100 | if (isset($decodedMessage['context'])) { 101 | $context = $this->contextSerializer->deserialize(json_encode($decodedMessage['context'])); 102 | } else { 103 | $context = new NullContext(); 104 | } 105 | 106 | $this->logger->info('processing notification'); 107 | 108 | return [$notification, $context]; 109 | } 110 | 111 | /** 112 | * @param MessageInterface $message 113 | * 114 | * @return PubSubRequest 115 | */ 116 | public function getRequest(MessageInterface $message) 117 | { 118 | $matched = $this->matcher->match($message->getChannel()); 119 | $request = new PubSubRequest($matched[0], $matched[1], $matched[2]); 120 | 121 | $this->logger->info(sprintf( 122 | 'Route %s matched with [%s]', 123 | $request->getRouteName(), 124 | implode(', ', array_map(function ($v, $k) { return sprintf("%s='%s'", $k, $v); }, $request->getAttributes()->all(), array_keys($request->getAttributes()->all()))) 125 | )); 126 | 127 | return $request; 128 | } 129 | 130 | /** 131 | * @param PusherInterface[] $pushers 132 | * @param RouteInterface $route 133 | * @param PubSubRequest $request 134 | * @param MessageInterface $message 135 | * @param NotificationInterface $notification 136 | * @param NotificationContextInterface $context 137 | * 138 | * @throws \Exception 139 | */ 140 | public function push( 141 | $pushers, 142 | RouteInterface $route, 143 | PubSubRequest $request, 144 | MessageInterface $message, 145 | NotificationInterface $notification, 146 | NotificationContextInterface $context 147 | ) { 148 | /** @var PusherInterface $pusher */ 149 | foreach ($pushers as $pusher) { 150 | if ($pusher instanceof ProcessorDelegate && count($route->getArgs()) >= 1) { 151 | $args = $route->getArgs(); 152 | 153 | foreach ($args as $attributeName => $processorService) { 154 | if (!in_array($attributeName, $availableAttributes = array_keys($request->getAttributes()->all()))) { 155 | throw new \Exception(sprintf( 156 | 'Undefined attribute %s, available are [%s]', 157 | $attributeName, 158 | $availableAttributes 159 | )); 160 | } 161 | 162 | if ('@' !== $processorService{0}) { 163 | throw new \Exception(sprintf( 164 | 'Your processor service must start with "@"' 165 | )); 166 | } 167 | 168 | $processor = $this->container->get(ltrim($processorService, '@')); 169 | 170 | if (!$processor instanceof ProcessorInterface) { 171 | throw new \Exception('Processor class must implement ProcessorInterface !'); 172 | } 173 | 174 | call_user_func([$pusher, 'addProcessor'], $attributeName, $processor); 175 | } 176 | } 177 | 178 | if ($pusher instanceof PusherLoopAwareInterface) { 179 | $pusher->setLoop($this->loop); 180 | } 181 | 182 | if ($pusher instanceof YoloInterface) { 183 | $yoloPush = new Yolo([$pusher, 'push'], [$message, $notification, $request, $context], null, 10); 184 | $yoloPush->setLogger($this->logger); 185 | $yoloPush->tryUntil($pusher); 186 | } else { 187 | $pusher->push($message, $notification, $request, $context); 188 | } 189 | } 190 | } 191 | 192 | /** 193 | * @param MessageInterface $message 194 | * 195 | * @throws \Exception 196 | */ 197 | public function __invoke(MessageInterface $message) 198 | { 199 | /* 200 | * @var NotificationInterface 201 | * @var NotificationContextInterface 202 | */ 203 | list($notification, $context) = $this->getNotification($message); 204 | $request = $this->getRequest($message); 205 | $route = $request->getRoute(); 206 | $pushers = $this->pusherRegistry->getPushers($route->getCallback()); 207 | 208 | $this->eventDispatcher->dispatch( 209 | NotificationEvents::NOTIFICATION_PUBLISHED, 210 | new NotificationPublishedEvent($message, $notification, $context, $request) 211 | ); 212 | 213 | $this->push($pushers, $route, $request, $message, $notification, $context); 214 | 215 | $this->logger->info(sprintf( 216 | 'Notification %s processed', 217 | $notification->getUuid() 218 | )); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /Server/ServerNotificationProcessorInterface.php: -------------------------------------------------------------------------------- 1 | broadcast($event, $exclude, $eligible); 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getName() 46 | { 47 | return 'gos.notification.topic'; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gos/notification-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Symfony Notification Bundle", 5 | "keywords": ["Notification Bundle", "Websocket", "PubSub", "Websocket", "Redis", "Real time"], 6 | "homepage": "https://github.com/GeniusesOfSymfony/NotificationBundle", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Johann Saunier", 11 | "email": "johann_27@hotmail.fr" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.3.2", 16 | "symfony/framework-bundle": "~2.4", 17 | "gos/web-socket-bundle": "^1.1", 18 | "snc/redis-bundle": "~1.1", 19 | "predis/predis": "~1.0", 20 | "predis/predis-async": "dev-master", 21 | "gos/yolo": "^0.2.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { "Gos\\Bundle\\NotificationBundle\\": "" } 25 | }, 26 | "extra": { 27 | "branch-alias": { 28 | "dev-master": "0.1.x-dev" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeniusesOfSymfony/NotificationBundle/fbeed0cf69356a351a29f777441952aefe10a22e/diagram.png -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeniusesOfSymfony/NotificationBundle/fbeed0cf69356a351a29f777441952aefe10a22e/screen.png --------------------------------------------------------------------------------