├── .github └── workflows │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── config └── megaphone.php ├── mix-manifest.json ├── package-lock.json ├── package.json ├── phpunit.xml ├── public └── css │ └── megaphone.css ├── resources ├── css │ └── megaphone.scss └── views │ ├── admin │ └── create-announcement.blade.php │ ├── components │ ├── icons │ │ ├── bell.blade.php │ │ ├── bells.blade.php │ │ ├── bullhorn.blade.php │ │ ├── close.blade.php │ │ ├── delete.blade.php │ │ ├── exclaimation.blade.php │ │ └── read.blade.php │ └── notification │ │ ├── date.blade.php │ │ ├── link.blade.php │ │ ├── notification.blade.php │ │ └── title.blade.php │ ├── icon.blade.php │ ├── megaphone.blade.php │ ├── popout.blade.php │ └── types │ ├── general.blade.php │ ├── important.blade.php │ └── new-feature.blade.php ├── social-image.png ├── src ├── Components │ └── Display.php ├── Console │ └── ClearOldNotifications.php ├── HasMegaphone.php ├── Livewire │ ├── Megaphone.php │ └── MegaphoneAdmin.php ├── MegaphoneServiceProvider.php ├── Types │ ├── BaseAnnouncement.php │ ├── General.php │ ├── Important.php │ └── NewFeature.php └── helpers.php ├── tailwind.config.js ├── tests ├── ConsoleClearNotificationsTest.php ├── HasMegaphoneTest.php ├── HelpersTest.php ├── MegaphoneAdminComponentTest.php ├── MegaphoneComponentTest.php ├── Pest.php └── Setup │ ├── TestCase.php │ ├── Types │ ├── CustomType.php │ └── SecondCustomType.php │ ├── User.php │ ├── migrations │ └── 2022_09_05_072807_create_notifications_table.php │ └── views │ └── custom-type.blade.php └── webpack.mix.js /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.operating-system }} 12 | strategy: 13 | matrix: 14 | operating-system: [ubuntu-24.04] 15 | php: ['8.1', '8.2', '8.3', '8.4'] 16 | 17 | name: P${{ matrix.php }} 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | 26 | - name: Validate composer.json and composer.lock 27 | run: composer validate 28 | 29 | - name: Install dependencies 30 | run: composer install --prefer-dist --no-progress 31 | 32 | - name: Run test suite 33 | run: composer run-script test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | .idea 4 | .DS_Store 5 | node_modules 6 | composer.lock 7 | .phpunit.result.cache 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [2.2.0] - 2025-04-16 7 | 8 | * Added 2 new SVG icons for delete and mark as read. [PR#43](https://github.com/mikebarlow/megaphone/pull/43) 9 | * Added ability to let users delete read notifications before they are auto cleared. [PR#43](https://github.com/mikebarlow/megaphone/pull/43) 10 | * Moved the config option for "auto clear after" into sub array along with options for deleting notifications. [PR#43](https://github.com/mikebarlow/megaphone/pull/43) 11 | * Deprecated old config option for "auto clear after", will be removed in a later version. [PR#43](https://github.com/mikebarlow/megaphone/pull/43) 12 | * Updated console command that clears notifications to look at new config option location, falls back to deprecated old location. [PR#43](https://github.com/mikebarlow/megaphone/pull/43) 13 | 14 | ## [2.1.0] - 2024-09-11 15 | 16 | * Moved SVG icons into anonymous components for easier reuse / overwriting.[PR#35](https://github.com/mikebarlow/megaphone/pull/35) 17 | * Reworked notification type templates into components. [PR#35](https://github.com/mikebarlow/megaphone/pull/35) 18 | * Added "mark all as read" feature for unread notifications. [PR#37](https://github.com/mikebarlow/megaphone/pull/37) 19 | * Added support for `wire:poll` to give the impression of a live component. [PR#38](https://github.com/mikebarlow/megaphone/pull/38) 20 | * Added `@megaphoneStyles` blade directive + improved default styles. [PR#39](https://github.com/mikebarlow/megaphone/pull/39) 21 | 22 | ## [2.0.0] - 2023-09-11 23 | 24 | * Updated PHP requirement to 8.1 and above (7.4 and 8.0 dropped) [PR#28](https://github.com/mikebarlow/megaphone/pull/28) 25 | * Updated to Livewire 3 [PR#28](https://github.com/mikebarlow/megaphone/pull/28) 26 | * Updated Testbench and Pest [PR#28](https://github.com/mikebarlow/megaphone/pull/28) 27 | 28 | ## [1.2.0] - 2023-02-25 29 | 30 | * Removed `public $user` from component and changed loading of announcements to prevent user model data exposure. [PR #22](https://github.com/mikebarlow/megaphone/pull/22) 31 | * Added ability to pass in the notifiableId via component render 32 | 33 | ## [1.1.0] - 2022-12-27 34 | 35 | * Improvement: New SVG Bell Icon [PR #17](https://github.com/mikebarlow/megaphone/pull/17) 36 | * Improvement: New config option to toggle unread notification count [PR #17](https://github.com/mikebarlow/megaphone/pull/17) 37 | * Fix: Notification badge styling with long unread notification counts [PR #17](https://github.com/mikebarlow/megaphone/pull/17) 38 | * Fix: Readme typo [PR #16](https://github.com/mikebarlow/megaphone/pull/16) 39 | 40 | ## [1.0.2] - 2022-11-15 41 | 42 | * Improvement: Removed the mouse over event for marking as read and added a button with click event to mark notification as read. 43 | 44 | ## [1.0.1] - 2022-11-13 45 | 46 | * Fix: Numerous Readme updates, fixing incorrect instructions. Demo also added! [PR #10](https://github.com/mikebarlow/megaphone/pull/10) 47 | * Fix: Spelling mistake in template caused bug with justified items [PR #5](https://github.com/mikebarlow/megaphone/pull/5) 48 | * Fix: Added support for PHP7.4 [PR #13](https://github.com/mikebarlow/megaphone/pull/13) 49 | 50 | ## [1.0.0] - 2022-09-19 51 | 52 | * Livewire Component to add "Bell" notification icon to your app powered by Laravel Notifications 53 | * Livewire Admin Component for sending manual notification to all users 54 | * Console command to clear old read notifications 55 | * Ability to define custom notification types 56 | * Uses TailwindCSS for styling with publishable templates to customise look and feel 57 | * 100% Test coverage 58 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions and suggestion are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/mikebarlow/megaphone). 6 | 7 | ## Pull Requests 8 | 9 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 10 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 11 | - **Document any change in behaviour** - Make sure the README / CHANGELOG and any other relevant documentation are kept up-to-date. 12 | - **Consider our release cycle** - We try to follow semver. Randomly breaking public APIs is not an option. 13 | - **Create topic branches** - Don't ask us to pull from your master branch. 14 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 15 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mike Barlow 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | Author 5 | Latest Version 6 | Software License 7 | Build Status 8 |

9 | 10 | Megaphone is a Laravel Livewire based component that uses the power of Laravels built in Notifications system to allow you to add "Bell Icon Notification System" to your app. 11 | 12 | Megaphone also ships with an Admin form component that allows you to send out a notification to all your users at once. Perfect for announcing new features or planned maintenance! 13 | 14 |

Server Management by ServerAuth.com

15 | 16 | ## Demo 17 | 18 | Before using Megaphone, a demo is available for you to view and try the Bell Icon component and Admin component. Aside from some minor styling changes to the Admin component so it fits the layout better, everything is "out the box" and will be exactly as is when you install Megaphone yourself. 19 | 20 | [View the Megaphone Demo](https://megaphone.mikebarlow.co.uk) 21 | 22 | ## Upgrade from 1.x 23 | 24 | Megaphone has been updated to support Livewire 3. This also means PHP requirements have been updated to match the requirements of Livewire 3 which means you need to be running PHP 8.1 or above (PHP 7.4 and 8.0 are no longer supported). 25 | Then make sure you follow the [Livewire upgrade guide](https://livewire.laravel.com/docs/upgrading). 26 | 27 | Update your Megaphone requirement to 2.* by running the following command in your terminal. 28 | 29 | ```bash 30 | composer require mbarlow/megaphone "^2.0" 31 | ``` 32 | 33 | ### AlpineJS 34 | 35 | If you previously included AlpineJS specifically for Megaphone then you can now remove that from your JS include because it is now bundled with Livewire. 36 | 37 | ### Template Changes 38 | 39 | If you are using the Admin component and are running with the Megaphone views published to your resources folder, you may wish to make these manual changes. 40 | 41 | Changes are all to `create-announcement.blade.php` which, if published, should be found at `resources/views/vendor/megaphone/admin/create-announcement.blade.php`. 42 | 43 | Find `wire:model="type"` and replace it with `wire:model.live="type"`. 44 | 45 | Find all instances of `wire:model.lazy` and replace it with `wire:model.blur`. 46 | 47 | ## Installation 48 | 49 | For the Livewire 2 version of Megaphone, see the 1.x versions of Megaphone and the [1.x branch](https://github.com/mikebarlow/megaphone/tree/1.x). 50 | 51 | Simply require the package via composer into your Laravel app. 52 | 53 | composer require mbarlow/megaphone 54 | 55 | If you aren't already using Laravel Livewire in your app, Megaphone should include the package via its dependency. Once composer has finished installing, make sure you run the [Livewire installation steps](https://livewire.laravel.com/docs/installation). 56 | 57 | Once Livewire has been installed, if you haven't already, ensure the [Laravel Database Notifications have been installed](https://laravel.com/docs/10.x/notifications#database-prerequisites) into your app. 58 | 59 | ```bash 60 | php artisan notifications:table 61 | 62 | php artisan migrate 63 | ``` 64 | 65 | This should create database table used to house your notifications. Next, make sure your User model (or relevant alternative model) has the notifiable trait added as mentioned in the [Laravel Documentation](https://laravel.com/docs/10.x/notifications#using-the-notifiable-trait) and also add the `HasMegaphone` trait provided by Megaphone. 66 | 67 | ```php 68 | 97 | ``` 98 | 99 | This will render a Bell Icon where the component has been placed. When clicked a static sidebar will appear on the right of the screen which will show all the existing and any new notifications to the user. 100 | 101 | ### Styling 102 | 103 | As default, Megaphone uses TailwindCSS to style the Bell Icon and the notification sidebar. If you are not using Tailwind you may want to include the Megaphone CSS into your template. Add the following blade directive to your sites ``. 104 | 105 | ```html 106 | @megaphoneStyles 107 | ``` 108 | 109 | If you are using TailwindCSS, make sure the Megaphone views are added to any Tailwind config to ensure the correct classes are compiled. 110 | 111 | If you wish to recompile Megaphone stylesheet, ensure you have node and npm installed and run `npm install`. To compile the styles then run `npx mix` as per the [Larave Mix Documentation](https://laravel-mix.com/docs/6.0/installation) 112 | 113 | ## Sending Notifications 114 | 115 | As default, Megaphone will only load notifications that have been registered within the Megaphone config file. Notifications shipped with Megaphone will be within `config('megaphone.types')`. This will be merged with the key values of `config('megaphone.customTypes')` to create the list of supported notifications. 116 | 117 | This means, you can see use the Laravel Notification system for other parts of your system without them appearing in the Megaphone notifications list. 118 | 119 | To send a Megaphone notification instantiate a new notification that extends `MBarlow\Megaphone\Types\BaseAnnouncement`. Megaphone ships with 3 as default, `MBarlow\Megaphone\Types\General`, `MBarlow\Megaphone\Types\Important` and `MBarlow\Megaphone\Types\NewFeature`. 120 | 121 | ```php 122 | $notification = new \MBarlow\Megaphone\Types\Important( 123 | 'Expected Downtime!', // Notification Title 124 | 'We are expecting some downtime today at around 15:00 UTC for some planned maintenance. Read more on a blog post!', // Notification Body 125 | 'https://example.com/link', // Optional: URL. Megaphone will add a link to this URL within the Notification display. 126 | 'Read More...' // Optional: Link Text. The text that will be shown on the link button. 127 | ); 128 | ``` 129 | 130 | Now, simply notify the required user of the notification as per the [Laravel Documentation](https://laravel.com/docs/10.x/notifications#using-the-notifiable-trait). 131 | 132 | ```php 133 | $user = \App\Models\User::find(1); 134 | 135 | $user->notify($notification); 136 | ``` 137 | 138 | Next time User ID 1 visits your app, their Bell Icon will have a red indicator with "1" inside to denote 1 new, unread notification. 139 | 140 | ## Custom Notifications 141 | 142 | As mentioned, you can add your own notification types to Megaphone. In order to do this, first create a new class within your application and make sure it extends `MBarlow\Megaphone\Types\BaseAnnouncement`, for example: 143 | 144 | ```php 145 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | {{ $announcement['title'] }} 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 180 | 181 | 182 | 183 | ``` 184 | 185 | Update the icon defined within `` and that should be it. Extend or modify the other components to change how other parts of your notification looks. 186 | 187 | Lastly, you need to tell Megaphone about this notification. Open up the Megaphone config file `config/megaphone.php` and find the `customTypes` attribute. This should be an associative array with the FQDN of the notification class as the key and the path to the view as the value. For example, 188 | 189 | ```php 190 | /* 191 | * Custom notification types specific to your App 192 | */ 193 | 'customTypes' => [ 194 | /* 195 | Associative array in the format of 196 | \Namespace\To\Notification::class => 'path.to.view', 197 | */ 198 | \App\Megaphone\MyCustomNotification::class => 'megaphone.my-custom-notification', 199 | ], 200 | ``` 201 | 202 | Now you can trigger the notification and a user will receive it via their Bell Icon. 203 | 204 | ```php 205 | $notification = new \App\Megaphone\MyCustomNotification( 206 | 'Hello World', 207 | 'This is a custom notification, hope you like our app!' 208 | ); 209 | 210 | $user = \App\Models\User::find(1); 211 | 212 | $user->notify($notification); 213 | ``` 214 | 215 | ## Admin Panel 216 | 217 | The usage shown so far is great for automatic flows, for example, letting a user know an action has completed in the background, "Your file is ready for download", "Your server has finished setting up", etc... but sometimes you may want to send notifications en masse. 218 | 219 | You may want to let users know that some downtime is expected for maintenance or that a cool new feature has launched. To cover these bases, Megaphone ships with an Admin component providing a form to send a notification to all users. 220 | 221 | To use the component simply create a new page within your admin area, or create a password-protected page within your application that only you as the application owners can access and drop in this Livewire component. 222 | 223 | ```html 224 | 225 | ``` 226 | 227 | Visit your page and you will be presented with a form, to first select the notification type and then fill out the title, body, link and link text. Once you have filled everything out, hit send to push the notification out to all users. 228 | 229 | The form has been styled with TailwindCSS so if it doesn't look styled correctly make sure to include TailwindCSS on the page that is showing the Admin component. Alternatively, the view file will have been published along with the other Megaphone assets so you can customise the form styling within `resources/views/vendor/megaphone/admin/create-announcement.blade.php`. 230 | 231 | ### Notification Type List 232 | 233 | As default, the notification type list is created by merging the array of default notifications within `config('megaphone.types')`, with the key values of the custom types array found within `config('megaphone.customTypes')`. 234 | 235 | If you have added a lot of custom types or if you have some system notifications that should not be selectable from this type list, you can build your own type list within the `adminTypeList` attribute of the megaphone config. 236 | 237 | Simply create an array of all the notification classes you wish to have available in the drop down list. 238 | 239 | ```php 240 | 'adminTypeList' => [ 241 | \MBarlow\Megaphone\Types\NewFeature::class, 242 | \App\Megaphone\MyCustomNotification::class, 243 | ], 244 | ``` 245 | 246 | This example would mean only the default New Feature notification and your Custom Notification would be available from the drop down menu. 247 | 248 | ### Type List - Notification Name 249 | 250 | The name shown for each notification in the drop down menu is calculated from the class name within the `BaseAnnouncement` class that all Megaphone notifications extend. If Megaphone is unable to calculate the name of a custom notification correctly, or you wish to label it differently within the Admin Component type list, you can define a `name()` method within your notification. Megaphone will use this to display the label. 251 | 252 | ```php 253 | command('megaphone:clear-announcements')->daily(); 276 | ``` 277 | 278 | This will clear any "read" Megaphone notifications older than 2 weeks old. This allows any user that may not have logged in for a number of weeks to still view the notification before it would be cleared. 279 | 280 | The 2-week time limit for old notifications is controlled via the Megaphone config file, `config('megaphone.clearNotifications.autoClearAfter')`. So should you wish to alter this cut off point, simply change this value to either extend or shorten the cut off. 281 | 282 | ## Changing Notifiable Model 283 | 284 | Because notifications can be attached to any model via the `Notifiable` trait, Megaphone too can be attached to any model providing the model also has the `Notifiable` trait attached. 285 | 286 | As default, Megaphone assumes you will be attaching it to the standard Laravel User model and when loading notifications, it will attempt to retrieve the ID of the logged in user from the Request object. 287 | 288 | If you are wanting to attach Megaphone to a Team model for example, change the `model` attribute of the published megaphone config file, `megaphone.php`. 289 | 290 | When rendering the Megaphone component, you will then need to pass in the ID of the notifiable model into the component so Megaphone can load the correct notifications 291 | 292 | ```html 293 | 294 | ``` 295 | 296 | 297 | 298 | ## Testing 299 | 300 | If you wish to run the tests, clone out the repository 301 | 302 | ```bash 303 | git clone git@github.com:mikebarlow/megaphone.git 304 | ``` 305 | 306 | Change to the root of the repository and run composer install with the dev dependencies 307 | 308 | ```bash 309 | cd megaphone 310 | composer install 311 | ``` 312 | 313 | A script is defined in the `composer.json` to run both the code sniffer and the unit tests 314 | 315 | ```bash 316 | composer run test 317 | ``` 318 | 319 | Or run them individually as required 320 | 321 | ```bash 322 | ./vendor/bin/pest 323 | ./vendor/bin/phpcs --standard=PSR2 src 324 | ``` 325 | 326 | ## Changelog 327 | 328 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 329 | 330 | ## Contributing 331 | 332 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 333 | 334 | ## License 335 | 336 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 337 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mbarlow/megaphone", 3 | "description": "UI and admin for Laravel Notifications.", 4 | "keywords": ["Announcements", "Bell Icon", "Notifications", "Livewire", "Laravel"], 5 | "homepage": "https://github.com/mikebarlow/megaphone", 6 | "license": "MIT", 7 | "version": "2.2.0", 8 | "authors": [ 9 | { 10 | "name": "Mike Barlow", 11 | "email": "mike@mikebarlow.co.uk", 12 | "role": "Developer" 13 | } 14 | ], 15 | "require": { 16 | "php": "^8.1", 17 | "livewire/livewire": "^3.0.1" 18 | }, 19 | "require-dev": { 20 | "squizlabs/php_codesniffer": "^3.7", 21 | "pestphp/pest-plugin-livewire": "^2.1", 22 | "orchestra/testbench": "^8.10", 23 | "pestphp/pest-plugin-faker": "^2.0", 24 | "pestphp/pest": "^2.16" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "MBarlow\\Megaphone\\": "src" 29 | }, 30 | "files": [ 31 | "src/helpers.php" 32 | ] 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "MBarlow\\Megaphone\\Tests\\": "tests" 37 | } 38 | }, 39 | "extra": { 40 | "laravel": { 41 | "providers": [ 42 | "MBarlow\\Megaphone\\MegaphoneServiceProvider" 43 | ] 44 | } 45 | }, 46 | "scripts": { 47 | "test": [ 48 | "./vendor/bin/phpcs --standard=PSR2 src", 49 | "./vendor/bin/pest" 50 | ] 51 | }, 52 | "config": { 53 | "allow-plugins": { 54 | "pestphp/pest-plugin": true 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /config/megaphone.php: -------------------------------------------------------------------------------- 1 | \App\Models\User::class, 8 | 9 | /* 10 | * Array of all the notification types to display in Megaphone 11 | */ 12 | 'types' => [ 13 | \MBarlow\Megaphone\Types\General::class, 14 | \MBarlow\Megaphone\Types\NewFeature::class, 15 | \MBarlow\Megaphone\Types\Important::class, 16 | ], 17 | 18 | /* 19 | * Custom notification types specific to your App 20 | */ 21 | 'customTypes' => [ 22 | /* 23 | Associative array in the format of 24 | \Namespace\To\Notification::class => 'path.to.view', 25 | */ 26 | ], 27 | 28 | /* 29 | * Array of Notification types available within MegaphoneAdmin Component or 30 | * leave as null to show all types / customTypes 31 | * 32 | * 'adminTypeList' => [ 33 | * \MBarlow\Megaphone\Types\NewFeature::class, 34 | * \MBarlow\Megaphone\Types\Important::class, 35 | * ], 36 | */ 37 | 'adminTypeList' => null, 38 | 39 | /* 40 | * Clear Megaphone notifications older than.... 41 | * @deprecated 42 | * @see "megaphone.clearNotifications.autoClearAfter" 43 | */ 44 | 'clearAfter' => '2 weeks', 45 | 46 | /* 47 | * Option for setting the icon to show actual count of unread Notifications or 48 | * show a dot instead 49 | */ 50 | 'showCount' => true, 51 | 52 | /* 53 | * Enable Livewire Poll feature for auto updating. 54 | * See livewire docs for poll option descriptions 55 | * @link https://livewire.laravel.com/docs/wire-poll 56 | */ 57 | 'poll' => [ 58 | 'enabled' => false, 59 | 60 | 'options' => [ 61 | 'time' => '15s', 62 | 'keepAlive' => false, 63 | 'viewportVisible' => false, 64 | ], 65 | ], 66 | 67 | /* 68 | * Options relating to the clearing out of notifications. 69 | * Enable the ability for users to delete notifications themselves. 70 | * Set the timeframe, after which read notifications will be auto cleared. 71 | */ 72 | 'clearNotifications' => [ 73 | 'userCanDelete' => false, 74 | 75 | 'autoClearAfter' => '2 weeks', 76 | ], 77 | ]; 78 | -------------------------------------------------------------------------------- /mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/public/css/megaphone.css": "/public/css/megaphone.css" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plugin", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mikebarlow/megaphone.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/mikebarlow/megaphones/issues" 18 | }, 19 | "homepage": "https://github.com/mikebarlow/megaphone", 20 | "devDependencies": { 21 | "laravel-mix": "^6.0.49", 22 | "sass": "^1.53.0", 23 | "sass-loader": "^12.6.0", 24 | "tailwindcss": "^3.1.6" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tests/ 7 | 8 | 9 | 10 | 11 | src/ 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/css/megaphone.css: -------------------------------------------------------------------------------- 1 | [x-cloak]{display:none!important}.megaphone .sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.megaphone .fixed{position:fixed}.megaphone .absolute{position:absolute}.megaphone .relative{position:relative}.megaphone .left-0{left:0}.megaphone .left-1{left:.25rem}.megaphone .left-2{left:.5rem}.megaphone .right-0{right:0}.megaphone .top-0{top:0}.megaphone .top-1{top:.25rem}.megaphone .top-2{top:.5rem}.megaphone .z-30{z-index:30}.megaphone .z-40{z-index:40}.megaphone .z-50{z-index:50}.megaphone .-mt-1{margin-top:-.25rem}.megaphone .mr-2{margin-right:.5rem}.megaphone .mr-5{margin-right:1.25rem}.megaphone .mt-2{margin-top:.5rem}.megaphone .mt-4{margin-top:1rem}.megaphone .flex{display:flex}.megaphone .inline-flex{display:inline-flex}.megaphone .aspect-square{aspect-ratio:1/1}.megaphone .h-12{height:3rem}.megaphone .h-3{height:.75rem}.megaphone .h-4{height:1rem}.megaphone .h-5{height:1.25rem}.megaphone .h-full{height:100%}.megaphone .h-screen{height:100vh}.megaphone .w-12{width:3rem}.megaphone .w-3{width:.75rem}.megaphone .w-4{width:1rem}.megaphone .w-5{width:1.25rem}.megaphone .w-full{width:100%}.megaphone .flex-shrink-0{flex-shrink:0}.megaphone .translate-x-0{--tw-translate-x:0px}.megaphone .translate-x-0,.megaphone .translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.megaphone .translate-x-full{--tw-translate-x:100%}.megaphone .transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.megaphone .cursor-pointer{cursor:pointer}.megaphone .items-center{align-items:center}.megaphone .justify-center{justify-content:center}.megaphone .justify-between{justify-content:space-between}.megaphone :is(.space-x-1>:not([hidden])~:not([hidden])){--tw-space-x-reverse:0;margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.25rem*var(--tw-space-x-reverse))}.megaphone .overflow-y-auto{overflow-y:auto}.megaphone .overflow-x-hidden{overflow-x:hidden}.megaphone .rounded{border-radius:.25rem}.megaphone .rounded-full{border-radius:9999px}.megaphone .rounded-md{border-radius:.375rem}.megaphone .rounded-xl{border-radius:.75rem}.megaphone .border{border-width:1px}.megaphone .border-b{border-bottom-width:1px}.megaphone .border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.megaphone .border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.megaphone .border-neutral-200{--tw-border-opacity:1;border-color:rgb(229 229 229/var(--tw-border-opacity))}.megaphone .bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.megaphone .bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.megaphone .bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity))}.megaphone .bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.megaphone .bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.megaphone .bg-opacity-20{--tw-bg-opacity:0.2}.megaphone .p-3{padding:.75rem}.megaphone .p-4{padding:1rem}.megaphone .px-1{padding-left:.25rem;padding-right:.25rem}.megaphone .px-2{padding-left:.5rem;padding-right:.5rem}.megaphone .px-3{padding-left:.75rem;padding-right:.75rem}.megaphone .py-1{padding-bottom:.25rem;padding-top:.25rem}.megaphone .py-16{padding-bottom:4rem;padding-top:4rem}.megaphone .pb-2{padding-bottom:.5rem}.megaphone .pt-2{padding-top:.5rem}.megaphone .pt-8{padding-top:2rem}.megaphone .text-center{text-align:center}.megaphone .font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.megaphone .text-base{font-size:1rem;line-height:1.5rem}.megaphone .text-sm{font-size:.875rem;line-height:1.25rem}.megaphone .text-xs{font-size:.75rem;line-height:1rem}.megaphone .font-medium{font-weight:500}.megaphone .font-semibold{font-weight:600}.megaphone .uppercase{text-transform:uppercase}.megaphone .leading-5{line-height:1.25rem}.megaphone .leading-6{line-height:1.5rem}.megaphone .leading-normal{line-height:1.5}.megaphone .text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.megaphone .text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.megaphone .text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.megaphone .text-neutral-600{--tw-text-opacity:1;color:rgb(82 82 82/var(--tw-text-opacity))}.megaphone .text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.megaphone .opacity-75{opacity:.75}.megaphone .shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.megaphone .outline-none{outline:2px solid transparent;outline-offset:2px}.megaphone .drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.megaphone .transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.megaphone .duration-300{transition-duration:.3s}.megaphone .ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.megaphone .hover\:bg-neutral-100:hover{--tw-bg-opacity:1;background-color:rgb(245 245 245/var(--tw-bg-opacity))}.megaphone .hover\:bg-neutral-200:hover{--tw-bg-opacity:1;background-color:rgb(229 229 229/var(--tw-bg-opacity))}.megaphone .hover\:text-red-500:hover{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.megaphone .hover\:text-red-700:hover{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.megaphone .focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}@media (prefers-reduced-motion:no-preference){@keyframes ping{75%,to{opacity:0;transform:scale(2)}}.megaphone .motion-safe\:animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite}}@media (min-width:640px){.megaphone .sm\:duration-300{transition-duration:.3s}}@media (min-width:1024px){.megaphone .lg\:w-7\/12{width:58.333333%}}@media (min-width:1280px){.megaphone .xl\:w-5\/12{width:41.666667%}}@media (min-width:1536px){.megaphone .\32xl\:w-3\/12{width:25%}} 2 | -------------------------------------------------------------------------------- /resources/css/megaphone.scss: -------------------------------------------------------------------------------- 1 | [x-cloak] { display: none !important; } 2 | 3 | @tailwind components; 4 | @tailwind utilities; 5 | -------------------------------------------------------------------------------- /resources/views/admin/create-announcement.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if(session()->has('megaphone_success')) 3 | 9 | @endif 10 | 11 |
12 |
13 | 14 | 23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 |
44 | 45 | 46 |
47 |
48 | -------------------------------------------------------------------------------- /resources/views/components/icons/bell.blade.php: -------------------------------------------------------------------------------- 1 | @props(['class' => 'h-full w-full fill-black dark:fill-white']) 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/bells.blade.php: -------------------------------------------------------------------------------- 1 | @props(['class' => 'w-4/5 h-4/5 fill-blue-600']) 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/bullhorn.blade.php: -------------------------------------------------------------------------------- 1 | @props(['class' => 'w-4/5 h-4/5 fill-green-600']) 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/close.blade.php: -------------------------------------------------------------------------------- 1 | @props(['class' => '']) 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/views/components/icons/delete.blade.php: -------------------------------------------------------------------------------- 1 | @props(['class' => '']) 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/exclaimation.blade.php: -------------------------------------------------------------------------------- 1 | @props(['class' => 'w-4/5 h-4/5 fill-red-600']) 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/views/components/icons/read.blade.php: -------------------------------------------------------------------------------- 1 | @props(['class' => '']) 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/views/components/notification/date.blade.php: -------------------------------------------------------------------------------- 1 | @props(['class' => 'focus:outline-none text-xs leading-3 pt-1 text-gray-500', 'createdAt',]) 2 | 3 |

4 | {{ $createdAt->diffForHumans() }} 5 |

6 | -------------------------------------------------------------------------------- /resources/views/components/notification/link.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'parentClass' => 'mt-2 text-right focus:outline-none text-xs leading-3 pt-1 pb-2 right-0', 3 | 'class' => 'cursor-pointer no-underline bg-gray-100 text-gray-800 rounded-md border border-gray-300 p-2 hover:bg-gray-300', 4 | 'link', 'newWindow', 'linkText', 5 | ]) 6 | 7 | @if(! empty($link)) 8 |

9 | 10 | {{ ! empty($linkText) ? $linkText : 'View' }} 11 | 12 |

13 | @endif 14 | -------------------------------------------------------------------------------- /resources/views/components/notification/notification.blade.php: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 |

7 | {{ $title }} 8 |

9 |

10 | {{ $body }} 11 |

12 |
13 |
14 | {{ $date }} 15 | 16 | {{ $link }} 17 |
18 |
19 | -------------------------------------------------------------------------------- /resources/views/components/notification/title.blade.php: -------------------------------------------------------------------------------- 1 | @props(['class' => 'text-indigo-700 font-bold', 'link',]) 2 | 3 | 4 | @if(! empty($link)) 5 | 6 | @endif 7 | 8 | {{ $slot }} 9 | 10 | @if(! empty($link)) 11 | 12 | @endif 13 | 14 | -------------------------------------------------------------------------------- /resources/views/icon.blade.php: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /resources/views/megaphone.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | @include('megaphone::icon') 4 | @include('megaphone::popout') 5 |
6 |
7 | -------------------------------------------------------------------------------- /resources/views/popout.blade.php: -------------------------------------------------------------------------------- 1 |
8 | 9 |
22 |
23 |
24 |

Notifications

25 | 28 |
29 | 30 |
31 | @if ($unread->count() > 0) 32 |
33 |

34 | Unread Notifications 35 |

36 | 37 | @if ($unread->count() > 1) 38 | 39 | @endif 40 |
41 | 42 | @foreach ($unread as $announcement) 43 |
44 | 45 | 46 | @if($announcement->read_at === null) 47 | 53 | @endif 54 |
55 | @endforeach 56 | @endif 57 | 58 | 59 | @if ($announcements->count() > 0) 60 |
61 |

62 | Previous Notifications 63 |

64 | 65 | @if($allowDelete) 66 | 70 | @endif 71 |
72 | @endif 73 | 74 | @foreach ($announcements as $announcement) 75 |
76 | 77 | 78 | @if($allowDelete) 79 | 85 | @endif 86 |
87 | @endforeach 88 | 89 | @if ($unread->count() === 0 && $announcements->count() === 0) 90 |
91 |
92 |

93 | No new announcements 94 |

95 |
96 |
97 | @endif 98 |
99 |
100 |
101 | 102 | -------------------------------------------------------------------------------- /resources/views/types/general.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ $announcement['title'] }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /resources/views/types/important.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ $announcement['title'] }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /resources/views/types/new-feature.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ $announcement['title'] }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /social-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebarlow/megaphone/8a4f1d66f0bd7997ead19bee55bef9fa4068420c/social-image.png -------------------------------------------------------------------------------- /src/Components/Display.php: -------------------------------------------------------------------------------- 1 | notification = $notification; 18 | } 19 | 20 | public function render() 21 | { 22 | if (! in_array( 23 | $this->notification->type, 24 | getMegaphoneTypes() 25 | )) { 26 | return ''; 27 | } 28 | 29 | $params = [ 30 | 'announcement' => array_merge( 31 | [ 32 | 'title' => '', 33 | 'body' => '', 34 | 'link' => '', 35 | 'linkNewWindow' => false, 36 | 'linkText' => 'View', 37 | ], 38 | $this->notification->data 39 | ), 40 | 'read_at' => $this->notification->read_at, 41 | 'created_at' => $this->notification->created_at, 42 | ]; 43 | 44 | $customTypes = config('megaphone.customTypes'); 45 | 46 | if (! empty($customTypes[$this->notification->type])) { 47 | $tpl = $customTypes[$this->notification->type]; 48 | } elseif ($this->notification->type === General::class) { 49 | $tpl = 'megaphone::types.general'; 50 | } elseif ($this->notification->type === NewFeature::class) { 51 | $tpl = 'megaphone::types.new-feature'; 52 | } elseif ($this->notification->type === Important::class) { 53 | $tpl = 'megaphone::types.important'; 54 | } else { 55 | return ''; 56 | } 57 | 58 | return view( 59 | $tpl, 60 | $params 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Console/ClearOldNotifications.php: -------------------------------------------------------------------------------- 1 | whereNotNull('read_at') 23 | ->where('created_at', '<', now()->sub($clearAfter)) 24 | ->delete(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/HasMegaphone.php: -------------------------------------------------------------------------------- 1 | notifications() 12 | ->whereIn( 13 | 'type', 14 | getMegaphoneTypes() 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Livewire/Megaphone.php: -------------------------------------------------------------------------------- 1 | 'required', 23 | 'announcements' => 'required', 24 | ]; 25 | 26 | public function mount(Request $request) 27 | { 28 | if (empty($this->notifiableId) && $request->user() !== null) { 29 | $this->notifiableId = $request->user()->id; 30 | } 31 | $this->showCount = config('megaphone.showCount', true); 32 | if (config()->has('megaphone.clearNotifications.userCanDelete')) { 33 | $this->allowDelete = config('megaphone.clearNotifications.userCanDelete'); 34 | } else { 35 | $this->allowDelete = false; 36 | } 37 | } 38 | 39 | public function getNotifiable() 40 | { 41 | return config('megaphone.model')::find($this->notifiableId); 42 | } 43 | 44 | public function loadAnnouncements($notifiable) 45 | { 46 | $this->unread = $this->announcements = collect([]); 47 | 48 | if ($notifiable === null || get_class($notifiable) !== config('megaphone.model')) { 49 | return; 50 | } 51 | 52 | $announcements = $notifiable->announcements()->get(); 53 | $this->unread = $announcements->whereNull('read_at'); 54 | $this->announcements = $announcements->whereNotNull('read_at'); 55 | } 56 | 57 | public function render() 58 | { 59 | $this->loadAnnouncements($this->getNotifiable()); 60 | return view('megaphone::megaphone'); 61 | } 62 | 63 | public function markAsRead(DatabaseNotification $notification) 64 | { 65 | $notification->markAsRead(); 66 | } 67 | 68 | public function markAllRead() 69 | { 70 | DatabaseNotification::query() 71 | ->where('notifiable_type', config('megaphone.model')) 72 | ->where('notifiable_id', $this->notifiableId) 73 | ->whereNull('read_at') 74 | ->update(['read_at' => now()]); 75 | } 76 | 77 | public function deleteNotification(DatabaseNotification $notification) 78 | { 79 | if (config('megaphone.clearNotifications.userCanDelete') === true) { 80 | $notification->delete(); 81 | } 82 | } 83 | 84 | public function deleteAllReadNotification() 85 | { 86 | if (config('megaphone.clearNotifications.userCanDelete') === true) { 87 | DatabaseNotification::query() 88 | ->where('notifiable_type', config('megaphone.model')) 89 | ->where('notifiable_id', $this->notifiableId) 90 | ->whereNotNull('read_at') 91 | ->delete(); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Livewire/MegaphoneAdmin.php: -------------------------------------------------------------------------------- 1 | notifTypes = collect(getMegaphoneAdminTypes()) 28 | ->mapWithKeys( 29 | function ($class) { 30 | return [ 31 | $class => $class::name(), 32 | ]; 33 | } 34 | ) 35 | ->toArray(); 36 | } 37 | 38 | public function render() 39 | { 40 | return view('megaphone::admin.create-announcement'); 41 | } 42 | 43 | public function send() 44 | { 45 | $this->validate(); 46 | 47 | $notification = new $this->type($this->title, $this->body, $this->link, $this->linkText); 48 | 49 | $this->getUsers()->each( 50 | function ($user) use ($notification) { 51 | $user->notify($notification); 52 | } 53 | ); 54 | 55 | session()->flash('megaphone_success', __('Notifications sent successfully!')); 56 | $this->resetExcept('notifTypes'); 57 | } 58 | 59 | protected function rules() 60 | { 61 | return [ 62 | 'type' => [ 63 | 'required', 64 | Rule::in( 65 | getMegaphoneTypes() 66 | ), 67 | ], 68 | 'title' => 'required', 69 | 'body' => 'required', 70 | ]; 71 | } 72 | 73 | protected function getUsers(): Collection 74 | { 75 | $modelClass = config('megaphone.model'); 76 | 77 | return (new $modelClass)->get(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/MegaphoneServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerConfigs(); 17 | } 18 | 19 | public function boot() 20 | { 21 | $this->bootBlade(); 22 | $this->bootConsole(); 23 | $this->bootLivewireComponents(); 24 | $this->bootPublishes(); 25 | $this->bootViews(); 26 | } 27 | 28 | protected function registerConfigs() 29 | { 30 | $this->mergeConfigFrom( 31 | __DIR__.'/../config/megaphone.php', 32 | 'megaphone' 33 | ); 34 | } 35 | 36 | protected function bootBlade() 37 | { 38 | Blade::componentNamespace('MBarlow\\Megaphone\\Components', 'megaphone'); 39 | 40 | Blade::directive( 41 | 'megaphonePoll', 42 | function () { 43 | return ''; 51 | } 52 | ); 53 | 54 | Blade::directive( 55 | 'megaphoneStyles', 56 | function () { 57 | return 'app->runningInConsole()) { 65 | $this->commands([ 66 | ClearOldNotifications::class, 67 | ]); 68 | } 69 | } 70 | 71 | protected function bootLivewireComponents() 72 | { 73 | Livewire::component('megaphone', Megaphone::class); 74 | Livewire::component('megaphone-admin', MegaphoneAdmin::class); 75 | } 76 | 77 | protected function bootPublishes() 78 | { 79 | $this->publishes([ 80 | __DIR__.'/../public' => public_path('vendor/megaphone'), 81 | __DIR__.'/../config/megaphone.php' => config_path('megaphone.php'), 82 | __DIR__.'/../resources/views' => resource_path('views/vendor/megaphone'), 83 | ], 'megaphone'); 84 | 85 | $this->publishes([ 86 | __DIR__.'/../public' => public_path('vendor/megaphone'), 87 | ], 'megaphone-assets'); 88 | 89 | $this->publishes([ 90 | __DIR__.'/../config/megaphone.php' => config_path('megaphone.php'), 91 | ], 'megaphone-config'); 92 | 93 | $this->publishes([ 94 | __DIR__.'/../resources/views' => resource_path('views/vendor/megaphone'), 95 | ], 'megaphone-views'); 96 | } 97 | 98 | protected function bootViews() 99 | { 100 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'megaphone'); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Types/BaseAnnouncement.php: -------------------------------------------------------------------------------- 1 | title = $title; 16 | $this->body = $body; 17 | $this->link = $link; 18 | $this->linkText = $linkText; 19 | } 20 | 21 | public function via($notifiable) 22 | { 23 | return ['database']; 24 | } 25 | 26 | public function toArray($notifiable) 27 | { 28 | return [ 29 | 'title' => $this->title, 30 | 'body' => $this->body, 31 | 'link' => $this->link, 32 | 'linkText' => $this->linkText, 33 | ]; 34 | } 35 | 36 | public static function name(): string 37 | { 38 | $elements = explode('\\', static::class); 39 | $class = end($elements); 40 | 41 | return implode(' ', Str::ucsplit($class)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Types/General.php: -------------------------------------------------------------------------------- 1 | createTestUser(); 7 | 8 | $this->createTestNotification( 9 | $user, 10 | \MBarlow\Megaphone\Types\General::class 11 | ); 12 | $this->assertDatabaseCount('notifications', 1); 13 | $user->unreadNotifications->markAsRead(); 14 | 15 | Carbon::setTestNow( 16 | Carbon::now()->addWeeks(3) 17 | ); 18 | 19 | $this->artisan('megaphone:clear-announcements')->assertSuccessful(); 20 | 21 | $this->assertDatabaseCount('notifications', 0); 22 | }); 23 | 24 | it('wont clear newer read notification', function () { 25 | $user = $this->createTestUser(); 26 | 27 | $this->createTestNotification( 28 | $user, 29 | \MBarlow\Megaphone\Types\General::class 30 | ); 31 | $this->assertDatabaseCount('notifications', 1); 32 | $user->unreadNotifications->markAsRead(); 33 | 34 | Carbon::setTestNow( 35 | Carbon::now()->addWeeks(1) 36 | ); 37 | 38 | $this->artisan('megaphone:clear-announcements')->assertSuccessful(); 39 | 40 | $this->assertDatabaseCount('notifications', 1); 41 | }); 42 | 43 | it('wont clear unread notification', function () { 44 | $user = $this->createTestUser(); 45 | 46 | $this->createTestNotification( 47 | $user, 48 | \MBarlow\Megaphone\Types\General::class 49 | ); 50 | $this->assertDatabaseCount('notifications', 1); 51 | 52 | Carbon::setTestNow( 53 | Carbon::now()->addWeeks(3) 54 | ); 55 | 56 | $this->artisan('megaphone:clear-announcements')->assertSuccessful(); 57 | 58 | $this->assertDatabaseCount('notifications', 1); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/HasMegaphoneTest.php: -------------------------------------------------------------------------------- 1 | set( 5 | 'megaphone.customTypes', 6 | [ 7 | \MBarlow\Megaphone\Tests\Setup\Types\CustomType::class => 'tests::custom-type', 8 | ] 9 | ); 10 | 11 | $this->actingAs( 12 | $user = $this->createTestUser() 13 | ); 14 | 15 | $this->createTestNotification( 16 | $user, 17 | \MBarlow\Megaphone\Types\General::class 18 | ); 19 | $this->createTestNotification( 20 | $user, 21 | \MBarlow\Megaphone\Tests\Setup\Types\CustomType::class 22 | ); 23 | $this->createTestNotification( 24 | $user, 25 | \MBarlow\Megaphone\Tests\Setup\Types\SecondCustomType::class 26 | ); 27 | 28 | $this->assertCount( 29 | 2, 30 | $user->announcements 31 | ); 32 | }); 33 | 34 | it('can get specific user notifications', function () { 35 | $user1 = $this->createTestUser(); 36 | 37 | $this->actingAs( 38 | $user2 = $this->createTestUser() 39 | ); 40 | 41 | $this->createTestNotification( 42 | $user1, 43 | \MBarlow\Megaphone\Types\General::class 44 | ); 45 | $this->createTestNotification( 46 | $user1, 47 | \MBarlow\Megaphone\Types\General::class 48 | ); 49 | $this->createTestNotification( 50 | $user2, 51 | \MBarlow\Megaphone\Types\General::class 52 | ); 53 | 54 | $this->assertCount( 55 | 1, 56 | $user2->announcements 57 | ); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/HelpersTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 5 | [ 6 | \MBarlow\Megaphone\Types\General::class, 7 | \MBarlow\Megaphone\Types\NewFeature::class, 8 | \MBarlow\Megaphone\Types\Important::class, 9 | ], 10 | getMegaphoneTypes() 11 | ); 12 | }); 13 | 14 | it('can merge default and custom megaphone types from config', function () { 15 | config()->set( 16 | 'megaphone.customTypes', 17 | [ 18 | \MBarlow\Megaphone\Tests\Setup\Types\CustomType::class => 'tests::custom-type', 19 | ] 20 | ); 21 | 22 | $this->assertEquals( 23 | [ 24 | \MBarlow\Megaphone\Types\General::class, 25 | \MBarlow\Megaphone\Types\NewFeature::class, 26 | \MBarlow\Megaphone\Types\Important::class, 27 | \MBarlow\Megaphone\Tests\Setup\Types\CustomType::class, 28 | ], 29 | getMegaphoneTypes() 30 | ); 31 | }); 32 | 33 | it('can fallback to getMegaphoneTypes if adminTypeList is null', function () { 34 | config()->set( 35 | 'megaphone.customTypes', 36 | [ 37 | \MBarlow\Megaphone\Tests\Setup\Types\CustomType::class => 'tests::custom-type', 38 | ] 39 | ); 40 | 41 | $this->assertEquals( 42 | [ 43 | \MBarlow\Megaphone\Types\General::class, 44 | \MBarlow\Megaphone\Types\NewFeature::class, 45 | \MBarlow\Megaphone\Types\Important::class, 46 | \MBarlow\Megaphone\Tests\Setup\Types\CustomType::class, 47 | ], 48 | getMegaphoneAdminTypes() 49 | ); 50 | }); 51 | 52 | it('can get the custom adminTypeList if an array', function () { 53 | config()->set( 54 | 'megaphone.adminTypeList', 55 | [ 56 | \MBarlow\Megaphone\Types\NewFeature::class, 57 | \MBarlow\Megaphone\Types\Important::class, 58 | ] 59 | ); 60 | 61 | $this->assertEquals( 62 | [ 63 | \MBarlow\Megaphone\Types\NewFeature::class, 64 | \MBarlow\Megaphone\Types\Important::class, 65 | ], 66 | getMegaphoneAdminTypes() 67 | ); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/MegaphoneAdminComponentTest.php: -------------------------------------------------------------------------------- 1 | livewire(MegaphoneAdmin::class) 7 | ->assertViewIs('megaphone::admin.create-announcement'); 8 | }); 9 | 10 | it('can send notifications to users', function () { 11 | $this->createTestUser(); 12 | $this->createTestUser(); 13 | 14 | $this->livewire(MegaphoneAdmin::class) 15 | ->set('type', \MBarlow\Megaphone\Types\General::class) 16 | ->set('title', 'Test Notification') 17 | ->set('body', 'This is a test notification') 18 | ->call('send') 19 | ->assertSee('Notifications sent successfully!'); 20 | 21 | $this->assertDatabaseCount('notifications', 2); 22 | }); 23 | 24 | it('can send notifications to users with custom type', function () { 25 | $this->createTestUser(); 26 | $this->createTestUser(); 27 | 28 | config()->set( 29 | 'megaphone.customTypes', 30 | [ 31 | \MBarlow\Megaphone\Tests\Setup\Types\CustomType::class => 'tests::custom-type', 32 | ] 33 | ); 34 | 35 | $this->livewire(MegaphoneAdmin::class) 36 | ->set('type', \MBarlow\Megaphone\Tests\Setup\Types\CustomType::class) 37 | ->set('title', 'Test Notification') 38 | ->set('body', 'This is a test notification') 39 | ->call('send') 40 | ->assertSee('Notifications sent successfully!'); 41 | 42 | $this->assertDatabaseCount('notifications', 2); 43 | }); 44 | 45 | 46 | it('can send notifications to user with link', function () { 47 | $this->createTestUser(); 48 | $this->createTestUser(); 49 | 50 | $this->livewire(MegaphoneAdmin::class) 51 | ->set('type', \MBarlow\Megaphone\Types\General::class) 52 | ->set('title', 'Test Notification') 53 | ->set('body', 'This is a test notification') 54 | ->set('link', 'https://github.com/mbarlow') 55 | ->set('linkText', 'My Github Profile') 56 | ->call('send') 57 | ->assertSee('Notifications sent successfully!'); 58 | 59 | $this->assertDatabaseCount('notifications', 2); 60 | }); 61 | 62 | it('fails validation when no title or body set', function () { 63 | $this->livewire(MegaphoneAdmin::class) 64 | ->set('type', \MBarlow\Megaphone\Types\General::class) 65 | ->call('send') 66 | ->assertHasErrors(['title', 'body']); 67 | }); 68 | 69 | it('fails validation when invalid / unregistered type set', function () { 70 | $this->livewire(MegaphoneAdmin::class) 71 | ->set('type', \MBarlow\Megaphone\Tests\Setup\Types\CustomType::class) 72 | ->set('title', 'Test Notification') 73 | ->set('body', 'This is a test notification') 74 | ->call('send') 75 | ->assertHasErrors(['type',]); 76 | }); 77 | -------------------------------------------------------------------------------- /tests/MegaphoneComponentTest.php: -------------------------------------------------------------------------------- 1 | livewire(Megaphone::class) 7 | ->assertViewIs('megaphone::megaphone') 8 | ->assertSeeHtml($this->bellSvgIcon()); 9 | }); 10 | 11 | it('can render the megaphone component with logged in user', function () { 12 | $this->actingAs( 13 | $this->createTestUser() 14 | ); 15 | 16 | $this->livewire(Megaphone::class) 17 | ->assertViewIs('megaphone::megaphone') 18 | ->assertSeeHtml($this->bellSvgIcon()); 19 | }); 20 | 21 | it('can render the megaphone component with notification count', function () { 22 | $this->actingAs( 23 | $user = $this->createTestUser() 24 | ); 25 | 26 | $this->createTestNotification( 27 | $user, 28 | \MBarlow\Megaphone\Types\General::class 29 | ); 30 | 31 | $this->livewire(Megaphone::class) 32 | ->assertViewIs('megaphone::megaphone') 33 | ->assertSeeHtml(' 34 | 35 | 36 | 37 | 38 | 1 39 | 40 | 41 | 42 | '); 43 | }); 44 | 45 | it('can render the megaphone component with notification dot', function () { 46 | config()->set( 47 | 'megaphone.showCount', 48 | false 49 | ); 50 | 51 | $this->actingAs( 52 | $user = $this->createTestUser() 53 | ); 54 | 55 | $this->createTestNotification( 56 | $user, 57 | \MBarlow\Megaphone\Types\General::class 58 | ); 59 | 60 | $this->livewire(Megaphone::class) 61 | ->assertViewIs('megaphone::megaphone') 62 | ->assertSeeHtml(' 63 | 64 | 65 | 66 | 67 | '); 68 | }); 69 | 70 | it('can load announcements', function () { 71 | $this->actingAs( 72 | $user = $this->createTestUser() 73 | ); 74 | 75 | $this->createTestNotification( 76 | $user, 77 | \MBarlow\Megaphone\Types\General::class 78 | ); 79 | $this->createTestNotification( 80 | $user, 81 | \MBarlow\Megaphone\Types\Important::class 82 | ); 83 | $user->unreadNotifications->first()->markAsRead(); 84 | 85 | 86 | $this->livewire(Megaphone::class) 87 | ->call('loadAnnouncements', $user) 88 | ->assertSet('unread', $user->announcements()->get()->whereNull('read_at')) 89 | ->assertSet('announcements', $user->readNotifications); 90 | }); 91 | 92 | it('can mark notification as read', function () { 93 | $this->actingAs( 94 | $user = $this->createTestUser() 95 | ); 96 | 97 | $this->createTestNotification( 98 | $user, 99 | \MBarlow\Megaphone\Types\General::class 100 | ); 101 | $this->createTestNotification( 102 | $user, 103 | \MBarlow\Megaphone\Types\Important::class 104 | ); 105 | $notification = $user->unreadNotifications->first(); 106 | 107 | $this->livewire(Megaphone::class) 108 | ->call('markAsRead', $notification) 109 | ->assertSet('unread', $user->announcements()->get()->whereNull('read_at')) 110 | ->assertSet('announcements', $user->readNotifications); 111 | }); 112 | 113 | it('can mark all notifications as read', function () { 114 | $this->actingAs( 115 | $user = $this->createTestUser() 116 | ); 117 | 118 | $this->createTestNotification( 119 | $user, 120 | \MBarlow\Megaphone\Types\General::class 121 | ); 122 | $this->createTestNotification( 123 | $user, 124 | \MBarlow\Megaphone\Types\Important::class 125 | ); 126 | 127 | $this->livewire(Megaphone::class) 128 | ->call('markAllRead') 129 | ->assertSet('unread', $user->announcements()->get()->whereNull('read_at')) 130 | ->assertSet('announcements', $user->readNotifications); 131 | }); 132 | 133 | it('doesn\'t shows mark all as read if only 1 or less', function () { 134 | $this->actingAs( 135 | $user = $this->createTestUser() 136 | ); 137 | 138 | $this->createTestNotification( 139 | $user, 140 | \MBarlow\Megaphone\Types\General::class 141 | ); 142 | 143 | $this->livewire(Megaphone::class) 144 | ->assertViewIs('megaphone::megaphone') 145 | ->assertDontSeeHtml('