├── .distignore ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.yml │ ├── Feature_request.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── actions │ ├── node │ │ └── action.yml │ └── php │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── coding-standards.yml │ ├── node-build.yml │ ├── release.yml │ └── test-suite.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc ├── .nvmrc ├── .prettierrc.js ├── .stylelintrc ├── .wp-env.json ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── babel.config.js ├── composer.json ├── composer.lock ├── includes ├── channels.php ├── class-activator.php ├── class-channel-registry.php ├── class-uninstaller.php ├── database │ └── class-schema.php ├── exceptions │ ├── class-runtime-exception.php │ └── interface-exception.php ├── factory │ ├── class-message.php │ ├── class-notification.php │ └── class-subscription.php ├── framework │ └── class-factory.php ├── helper │ └── class-serde.php ├── image │ ├── class-base-image.php │ └── interface-image.php ├── interface-notification.php ├── interface-status.php ├── load.php ├── model │ ├── class-channel.php │ ├── class-message.php │ ├── class-notification.php │ └── class-subscription.php ├── persistence │ ├── class-abstract-notification-repository.php │ ├── class-wpdb-notification-repository.php │ ├── interface-notification-repository.php │ └── interface-order.php └── restapi │ ├── class-channel-controller.php │ ├── class-notification-controller.php │ └── class-subscription-controller.php ├── jest.config.js ├── languages └── .gitkeep ├── package-lock.json ├── package.json ├── phpcs.xml.dist ├── phpunit.xml.dist ├── readme.txt ├── src ├── components │ ├── drawer │ │ └── index.tsx │ ├── hub-icon │ │ └── index.tsx │ ├── notice-area │ │ ├── footer │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── section-header │ │ │ └── index.tsx │ ├── notice-empty │ │ └── index.tsx │ ├── notice-loop │ │ └── index.tsx │ ├── notice │ │ ├── actions │ │ │ └── index.tsx │ │ ├── icon │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── meta │ │ │ └── index.tsx │ ├── notification-hub │ │ └── index.tsx │ └── unread-dot │ │ └── index.tsx ├── constants.ts ├── store │ ├── actions.ts │ ├── constants.ts │ ├── controls.ts │ ├── index.ts │ ├── reducer.ts │ ├── resolvers.ts │ ├── selectors.ts │ └── utils.ts ├── styles │ ├── components │ │ ├── bullet.scss │ │ └── notification.scss │ ├── dashboard │ │ └── notice.scss │ ├── hub │ │ ├── admin-bar.scss │ │ ├── elements.scss │ │ ├── layout.scss │ │ └── notice.scss │ ├── vars.scss │ └── wp-notifications.scss ├── types.ts ├── utils │ ├── guards.ts │ ├── index.ts │ ├── init.tsx │ └── sanitization.ts └── wp-notifications.ts ├── storybook ├── .babelrc.json ├── .npmrc ├── .storybook │ ├── main.js │ ├── manager.js │ ├── preview.js │ └── wp-notifications-theme.js ├── fake_api.json ├── package.json ├── stories │ ├── Dash-multiple.stories.jsx │ ├── Dash-single.stories.jsx │ ├── Hub-empty.stories.jsx │ ├── Hub-multiple.stories.jsx │ ├── Hub-single.stories.jsx │ ├── Introduction.stories.mdx │ ├── assets │ │ ├── WordPressLogo.svg │ │ ├── code.svg │ │ ├── dashicons.css │ │ ├── env.svg │ │ ├── i.svg │ │ ├── logo.afphoto │ │ ├── logo.svg │ │ ├── slack.svg │ │ └── wp-core │ │ │ ├── admin-bar.css │ │ │ ├── admin-menu.css │ │ │ ├── buttons.css │ │ │ ├── common.css │ │ │ ├── dashboard.css │ │ │ ├── dashicons.css │ │ │ ├── edit.css │ │ │ ├── fonts │ │ │ ├── dashicons.eot │ │ │ ├── dashicons.svg │ │ │ ├── dashicons.ttf │ │ │ ├── dashicons.woff │ │ │ └── dashicons.woff2 │ │ │ ├── images │ │ │ ├── about-header-about.svg │ │ │ ├── about-texture.png │ │ │ ├── resize-2x.gif │ │ │ ├── resize-rtl-2x.gif │ │ │ ├── resize-rtl.gif │ │ │ ├── resize.gif │ │ │ ├── spinner-2x.gif │ │ │ ├── spinner.gif │ │ │ ├── stars-2x.png │ │ │ └── stars.png │ │ │ ├── nav-menus.css │ │ │ ├── normalize.css │ │ │ └── site-health.css │ └── docs │ │ ├── contributors │ │ ├── databaseSchema.stories.mdx │ │ └── develop.md │ │ ├── database-schema.md │ │ ├── databaseSchema.stories.mdx │ │ ├── internal-api.md │ │ ├── internalApi.stories.mdx │ │ ├── rest-api.md │ │ ├── restApi.stories.mdx │ │ ├── translations.md │ │ └── translations.stories.mdx └── tsconfig.json ├── tests ├── bin │ └── install-wp-tests.sh ├── jse2e │ └── main.test.js ├── jsunit │ └── main.test.js └── phpunit │ ├── includes │ ├── bootstrap.php │ ├── class-db-testcase.php │ └── class-testcase.php │ └── tests │ ├── factory │ ├── test-factory-message.php │ ├── test-factory-notification.php │ └── test-factory-subscription.php │ ├── model │ ├── test-model-channel.php │ ├── test-model-message.php │ ├── test-model-notification.php │ └── test-model-subscription.php │ ├── rest-api │ ├── test-channel-controller.php │ ├── test-notification-controller.php │ └── test-subscription-controller.php │ ├── test-activator.php │ ├── test-base-image.php │ ├── test-channel-registry.php │ └── test-uninstaller.php ├── tsconfig.eslint.json ├── tsconfig.json ├── types └── global.d.ts ├── webpack.config.js └── wp-feature-notifications.php /.distignore: -------------------------------------------------------------------------------- 1 | /.git 2 | .gitignore 3 | .gitattributes 4 | 5 | /.idea 6 | /.github 7 | /.storybook 8 | /.vscode 9 | /.wordpress-org 10 | 11 | /.husky 12 | /docs 13 | /node_modules 14 | /src 15 | /storybook 16 | /tests 17 | /types 18 | /vendor/bin 19 | /vendor/composer/installers 20 | /vendor/**/*.phar 21 | /wp-feature-notifications 22 | 23 | .distignore 24 | .editorconfig 25 | .eslintignore 26 | .eslintrc.js 27 | .gitattributes 28 | .gitignore 29 | .lintstagedrc 30 | .nvmrc 31 | .prettierignore 32 | .prettierrc.js 33 | .stylelintrc 34 | .wp-env.json 35 | 36 | babel.config.js 37 | composer.json 38 | composer.lock 39 | jest.config.js 40 | package-lock.json 41 | package.json 42 | phpcs.xml.dist 43 | phpunit.xml.dist 44 | CONTRIBUTING.md 45 | README.md 46 | readme.md 47 | tsconfig.eslint.json 48 | tsconfig.json 49 | webpack.config.js 50 | wp-feature-notifications.zip 51 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = tab 15 | 16 | [*.{yml,yaml}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.{gradle,java,kt}] 21 | indent_style = space 22 | 23 | [packages/react-native-*/**.xml] 24 | indent_style = space 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ESLint presets 3 | */ 4 | module.exports = { 5 | root: true, 6 | parser: '@typescript-eslint/parser', 7 | parserOptions: { 8 | project: [ './tsconfig.json', 'tsconfig.eslint.json' ], 9 | }, 10 | extends: [ 'plugin:@wordpress/eslint-plugin/recommended' ], 11 | rules: { 12 | 'import/order': [ 13 | 'error', 14 | { 15 | alphabetize: { 16 | order: 'asc', 17 | caseInsensitive: true, 18 | }, 19 | 'newlines-between': 'always', 20 | groups: [ 'builtin', 'external', 'parent', 'sibling', 'index' ], 21 | pathGroups: [ 22 | { 23 | pattern: '@wordpress/**', 24 | group: 'external', 25 | }, 26 | ], 27 | pathGroupsExcludedImportTypes: [ 'builtin' ], 28 | }, 29 | ], 30 | }, 31 | overrides: [ 32 | { 33 | files: 'tests/**/*', 34 | rules: { 35 | 'no-undef': 'off', 36 | }, 37 | }, 38 | ], 39 | settings: { 40 | 'import/parsers': { 41 | '@typescript-eslint/parser': [ '.js', '.jsx', '.ts', '.tsx' ], 42 | }, 43 | 'import/resolver': { 44 | typescript: { 45 | alwaysTryTypes: true, 46 | project: [ './tsconfig.json', 'tsconfig.eslint.json' ], 47 | }, 48 | }, 49 | }, 50 | env: { 51 | browser: true, 52 | es2021: true, 53 | jest: true, 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # exclude vendored and generated files 2 | package-lock.json linguist-generated 3 | /storybook/stories/assets/** linguist-vendored 4 | /docs/** linguist-documentation 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a bug with WP Feature Notifications 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thanks for taking the time to fill out this bug report! If this is a security issue, please report it in HackerOne instead: https://hackerone.com/wordpress 8 | - type: textarea 9 | attributes: 10 | label: Description 11 | description: Please write a brief description of the bug, including what you expect to happen and what is currently happening. 12 | placeholder: | 13 | Feature '...' is not working properly. I expect '...' to happen, but '...' happens instead 14 | validations: 15 | required: true 16 | 17 | - type: textarea 18 | attributes: 19 | label: Step-by-step reproduction instructions 20 | description: Please write the steps needed to reproduce the bug. 21 | placeholder: | 22 | 1. Go to '...' 23 | 2. Click on '...' 24 | 3. Scroll down to '...' 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | attributes: 30 | label: Screenshots, screen recording, code snippet 31 | description: | 32 | If possible, please upload a screenshot or screen recording which demonstrates the bug. You can use LIEcap to create a GIF screen recording: https://www.cockos.com/licecap/ 33 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 34 | If this bug is to related to a developer API, please share a code snippet that demonstrates the issue. For small snippets paste it directly here, or you can use GitHub Gist to share multiple code files: https://gist.github.com 35 | Please ensure the shared code can be used by a developer to reproduce the issue — ideally it can be copied into a local development environment or executed in a browser console to help debug the issue. 36 | validations: 37 | required: false 38 | 39 | - type: textarea 40 | attributes: 41 | label: Environment info 42 | description: | 43 | Please list what WP Feature Notifications version you are using. 44 | placeholder: | 45 | - WordPress version, WP Feature Notifications version, and active Theme you are using. 46 | - Browser(s) are you seeing the problem on. 47 | - Device you are using and operating system (e.g. "Desktop with Windows 10", "iPhone with iOS 14", etc.). 48 | - Any other active plugins and your hosting environment (if applicable) 49 | validations: 50 | required: false 51 | 52 | - type: dropdown 53 | id: existing 54 | attributes: 55 | label: Please confirm that you have searched existing issues in the repo. 56 | description: You can do this by searching https://github.com/WordPress/wp-feature-notifications/issues and making sure the bug is not related to another plugin. 57 | multiple: true 58 | options: 59 | - 'Yes' 60 | - 'No' 61 | validations: 62 | required: true 63 | 64 | - type: dropdown 65 | id: plugins 66 | attributes: 67 | label: Please confirm that you have tested with all plugins deactivated except WP Feature Notifications. 68 | multiple: true 69 | options: 70 | - 'Yes' 71 | - 'No' 72 | validations: 73 | required: true 74 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Propose an idea for a feature or an enhancement 3 | body: 4 | - type: textarea 5 | attributes: 6 | label: What problem does this address? 7 | description: Please describe if this feature or enhancement is related to a current problem or pain point. For example, "I'm always frustrated when ..." or "It is currently difficult to ...". 8 | placeholder: | 9 | It is currently difficult to ... 10 | validations: 11 | required: true 12 | 13 | - type: textarea 14 | attributes: 15 | label: What is your proposed solution? 16 | description: Please outline the feature or enhancement that you want and how it addresses any problem identified above. 17 | validations: 18 | required: true 19 | 20 | - type: dropdown 21 | id: existing 22 | attributes: 23 | label: Please confirm that you have searched existing issues in the repo. 24 | description: You can do this by searching https://github.com/WordPress/wp-feature-notifications/issues. 25 | multiple: true 26 | options: 27 | - 'Yes' 28 | - 'No' 29 | validations: 30 | required: true 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 3 | 4 | ## What? 5 | 6 | 7 | ## Why? 8 | 10 | 11 | ## How? 12 | 13 | 14 | ## Testing Instructions 15 | 16 | 17 | 18 | 19 | 20 | ## Screenshots or screencast 21 | -------------------------------------------------------------------------------- /.github/actions/node/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup Node 2 | description: Setup Node.js to a specific version and cache dependencies 3 | 4 | inputs: 5 | node-version: 6 | description: 'Node.js version the action should use' 7 | required: true 8 | default: '16' 9 | 10 | runs: 11 | using: 'composite' 12 | steps: 13 | - name: Setup node 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: ${{ inputs.node-version }} 17 | 18 | - name: Get npm cache directory 19 | id: npm-cache-dir 20 | shell: bash 21 | run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT} 22 | 23 | - uses: actions/cache@v3 24 | id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true' 25 | with: 26 | path: ${{ steps.npm-cache-dir.outputs.dir }} 27 | key: ${{ runner.os }}-node-${{ inputs.node-version }}-${{ hashFiles('**/package-lock.json') }} 28 | restore-keys: | 29 | ${{ runner.os }}-node-${{ inputs.node-version }} 30 | 31 | - name: Install Packages 32 | shell: bash 33 | run: npm ci 34 | -------------------------------------------------------------------------------- /.github/actions/php/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup PHP 2 | 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - name: Setup PHP 7 | uses: shivammathur/setup-php@v2 8 | with: 9 | php-version: ${{ matrix.php }} 10 | tools: composer 11 | extensions: mysql 12 | coverage: none 13 | 14 | - name: Get Composer cache directory 15 | id: composer-cache 16 | shell: bash 17 | run: echo "dir=$(composer config cache-files-dir)" >> ${GITHUB_OUTPUT} 18 | 19 | - name: Cache Composer packages 20 | uses: actions/cache@v3 21 | with: 22 | path: ${{ steps.composer-cache.outputs.dir }} 23 | key: ${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} 24 | restore-keys: | 25 | ${{ runner.os }}-php-${{ matrix.php }}- 26 | 27 | - name: Install dependencies 28 | shell: bash 29 | run: composer install --prefer-dist --no-progress 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: 'composer' 5 | directory: '/' 6 | schedule: 7 | interval: 'weekly' 8 | day: 'thursday' 9 | versioning-strategy: lockfile-only 10 | open-pull-requests-limit: 10 11 | commit-message: 12 | prefix: 'Composer' 13 | allow: 14 | - dependency-type: 'direct' 15 | groups: 16 | phpunit: 17 | patterns: 18 | - 'phpunit/phpunit' 19 | - 'wp-phpunit/wp-phpunit' 20 | - 'yoast/phpunit-polyfills' 21 | 22 | - package-ecosystem: 'npm' 23 | directory: '/' 24 | schedule: 25 | interval: 'weekly' 26 | day: 'thursday' 27 | versioning-strategy: lockfile-only 28 | open-pull-requests-limit: 10 29 | commit-message: 30 | prefix: 'npm' 31 | allow: 32 | - dependency-type: 'direct' 33 | - dependency-name: '@wordpress/*' 34 | dependency-type: 'all' 35 | groups: 36 | wordpress: 37 | patterns: 38 | - '@wordpress/*' 39 | storybook: 40 | patterns: 41 | - '@storybook/*' 42 | -------------------------------------------------------------------------------- /.github/workflows/coding-standards.yml: -------------------------------------------------------------------------------- 1 | name: Coding Standards 2 | 3 | on: 4 | pull_request: 5 | branches: [develop, trunk] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | lint: 10 | name: Check Coding Standards 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Setup Node.js 17 | uses: ./.github/actions/node 18 | 19 | - name: JS check 20 | shell: bash 21 | run: npm run lint:js 22 | 23 | - name: WPCS check 24 | uses: 10up/wpcs-action@stable 25 | with: 26 | enable_warnings: true 27 | use_local_config: true 28 | -------------------------------------------------------------------------------- /.github/workflows/node-build.yml: -------------------------------------------------------------------------------- 1 | name: JS Continuous Integration 2 | 3 | on: 4 | pull_request: 5 | branches: [develop, trunk] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | name: Build plugin 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 15 | node-version: [16.x, 18.x] 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | 22 | - name: Setup Node.js 23 | uses: ./.github/actions/node 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: Build package 28 | shell: bash 29 | run: npm run build --if-present 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | 8 | env: 9 | PLUGIN_SLUG: wp-feature-notifications 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | release: 16 | name: Create Release 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | 23 | - name: Setup Node.js 24 | uses: ./.github/actions/node 25 | 26 | - name: Build package 27 | shell: bash 28 | run: npm run build --if-present 29 | 30 | - name: Create artifacts 31 | shell: bash 32 | run: npm run plugin-zip 33 | 34 | - name: Release 35 | uses: softprops/action-gh-release@v1 36 | with: 37 | name: github.ref 38 | files: ${{ env.PLUGIN_SLUG }}.zip 39 | fail_on_unmatched_files: true 40 | target_commitish: trunk 41 | generate_release_notes: true 42 | draft: true 43 | 44 | build-docs: 45 | name: Deploy documentation to GitHub Pages 46 | runs-on: ubuntu-latest 47 | needs: release 48 | 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v3 52 | 53 | - name: Setup Node.js 54 | uses: ./.github/actions/node 55 | 56 | - name: Build Storybook 57 | shell: bash 58 | run: npm run build:storybook 59 | 60 | - name: Deploy 61 | uses: peaceiris/actions-gh-pages@64b46b4226a4a12da2239ba3ea5aa73e3163c75b # v3.9.1 62 | with: 63 | github_token: ${{ secrets.GITHUB_TOKEN }} 64 | publish_dir: ./docs 65 | force_orphan: true 66 | -------------------------------------------------------------------------------- /.github/workflows/test-suite.yml: -------------------------------------------------------------------------------- 1 | name: Test Suite 2 | 3 | on: 4 | pull_request: 5 | branches: [develop, trunk] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | test: 10 | name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} 11 | timeout-minutes: 15 12 | runs-on: ubuntu-latest 13 | env: 14 | WP_TESTS_DIR: /tmp/wordpress-tests-lib 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | php: ['7.4', '8.0', '8.1', '8.2'] 19 | wp: ['latest'] 20 | services: 21 | database: 22 | image: mysql:5.6 23 | env: 24 | MYSQL_ROOT_PASSWORD: wordpress 25 | ports: 26 | - 3306:3306 27 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 28 | 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v3 32 | 33 | - name: Setup Node.js 34 | uses: ./.github/actions/node 35 | 36 | - name: Setup PHP 37 | uses: ./.github/actions/php 38 | 39 | - name: Install WordPress and initialize database 40 | run: ./tests/bin/install-wp-tests.sh wp_notify_tests root wordpress 127.0.0.1 latest 41 | 42 | - name: Run PHP Unit tests 43 | run: composer run test 44 | 45 | - name: Starting the WordPress Environment 46 | run: npx wp-env start 47 | 48 | - name: Running the JavaScript tests 49 | run: npm run test:js 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS fixes 2 | .DS_Store 3 | 4 | # IDE-specific 5 | .phpunit.result.cachecghooks.lock 6 | 7 | # Vendor code 8 | /node_modules/ 9 | /vendor/ 10 | 11 | # Build Files 12 | /build/ 13 | /docs/ 14 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "**/*.(json|yml|yaml)": "prettier --write", 3 | "**/*.(css|scss|sass)": "stylelint --fix", 4 | "**/*.(js|jsx|cjs|mjs|ts|tsx)": "eslint --ext .js,.jsx,.cjs,.mjs,.ts,.tsx --fix", 5 | "**/*.php": "php vendor/bin/phpcbf --standard=phpcs.xml.dist -s --report=summary" 6 | } 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/gallium -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // Import the default config file and expose it in the project root. 2 | // Useful for editor integrations. 3 | module.exports = require( '@wordpress/prettier-config' ); 4 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "@wordpress/stylelint-config/scss" ] 3 | } 4 | -------------------------------------------------------------------------------- /.wp-env.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ "." ] 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We would love your input! We want to make contributing to this project as easy and transparent as possible, whether it’s: 4 | 5 | - Reporting a bug 6 | - Testing the plugin 7 | - Discussing the current state, features, improvements 8 | - Submitting a fix or a new feature 9 | 10 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 11 | 12 | ## Philosophy and License 13 | 14 | WP Feature Notifications is an open source project, building on the WordPress software and the efforts of many past contributors. 15 | 16 | Its end goal is to become a part of WordPress core, and as such it is recommended to familiarise yourself with the [WordPress Core handbook](https://make.wordpress.org/core/handbook/) 17 | 18 | By contributing, you agree that your contributions will be licensed under its [GPLv2 License](LICENSE.md). 19 | 20 | ## Code of Conduct 21 | 22 | As a WordPress project, WP Feature Notifications follows the [WordPress Etiquette principles](https://wordpress.org/about/etiquette/), reproduced below: 23 | 24 | > In the WordPress open source project, we realize that our biggest asset is the community that we foster. The project, as a whole, follows these basic philosophical principles from The Cathedral and The Bazaar. 25 | > - Contributions to the WordPress open source project are for the benefit of the WordPress community as a whole, not specific businesses or individuals. All actions taken as a contributor should be made with the best interests of the community in mind. 26 | > - Participation in the WordPress open source project is open to all who wish to join, regardless of ability, skill, financial status, or any other criteria. 27 | > - The WordPress open source project is a volunteer-run community. Even in cases where contributors are sponsored by companies, that time is donated for the benefit of the entire open source community. 28 | > - Any member of the community can donate their time and contribute to the project in any form including design, code, documentation, community building, etc. For more information, go to [make.wordpress.org](https://make.wordpress.org). 29 | > - The WordPress open source community cares about diversity. We strive to maintain a welcoming environment where everyone can feel included, by keeping communication free of discrimination, incitement to violence, promotion of hate, and unwelcoming behavior. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordPress Feature Project - Notifications 2 | 3 | > A feature plugin for WordPress, which aims to create a new (better) way to manage and deliver notifications to the relevant audience. 4 | 5 | - Contributors: schlessera, psykro, raaaahman, danbilauca, Sephsekla, erikyo, JasonTheAdams, johnhooks 6 | - Tags: feature-notifications 7 | - Requires at least: 6.2 8 | - Tested up to: 6.2 9 | - Requires PHP: 7.4 10 | - License: GPLv2 or later 11 | - License URI: https://www.gnu.org/licenses/gpl-2.0.html 12 | 13 | See also [Trac ticket #43484](https://core.trac.wordpress.org/ticket/43484). 14 | 15 | ## Contributing to the project 16 | 17 | Want to get involved? Join our weekly office hours every Wednesday at 15:00 UTC in the [#feature-notifications](https://wordpress.slack.com/messages/C2K1C71FE) channel of the [Make WordPress Slack](https://make.wordpress.org/chat/). 18 | 19 | Please be sure to read our [contribution guidelines](CONTRIBUTING.md) before getting started. 20 | 21 | ### Prerequisites 22 | 23 | - [NodeJS](https://nodejs.org/en/download/) 24 | - [Composer](https://getcomposer.org/download/) 25 | - [wp-env](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/) (optional) 26 | - [Docker](https://docs.docker.com/get-docker/) (optional) 27 | 28 | We recommend using [nvm](https://github.com/nvm-sh/nvm) to ensure a compatible node version. 29 | 30 | ### Installation 31 | 32 | ```bash 33 | $ git clone https://github.com/WordPress/wp-feature-notifications.git 34 | $ cd wp-feature-notifications 35 | $ nvm use && npm i && composer install 36 | $ wp-env start 37 | ``` 38 | 39 | We take advantage of [wp-scripts](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/) to compile scripts and styles for this plugin. 40 | You will mainly need these two commands: 41 | 42 | `npm run build` - Transforms your code according to the configuration provided, so it’s ready for production and optimized for the best performance. 43 | `npm run start`- Transforms your code according to the configuration provided, so it’s ready for development. The script will automatically rebuild if you make changes to the code, and you will see the build errors in the console. 44 | 45 | ## Issue Workflow 46 | 47 | ### Creating an issue 48 | 49 | Have an improvement, suggestion or bug? The first step is to [open an issue](https://github.com/WordPress/wp-feature-notifications/issues). New ideas and new contributors are very welcome! Please be sure to fill out all available fields, and provide as much detail as possible. 50 | 51 | Once your issue has been opened, it will be triaged, labelled and moved to the relevant [project board](https://github.com/WordPress/wp-feature-notifications/projects?type=classic). 52 | 53 | > [!IMPORTANT] 54 | > If your issue is a security vulnerabilty, please practice responsible disclosure and submit this at https://github.com/WordPress/wp-feature-notifications/security/advisories/new. 55 | 56 | 57 | ###  Working on an issue 58 | 59 | Please ensure that nobody else is already working on an issue before starting work, in order to avoid duplication of effort. If in doubt, it's best to ask in the issue itself! When starting work, **you should assign the issue to yourself** to make this as clear as possible. 60 | 61 | If you are contributing code, be sure to follow our [Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/) for both JavaScript and PHP. 62 | 63 | You should create one pull request for each indvidual issue you are working on. Make sure to [link it to the issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) for easy tracking. Create a draft pull request as early as possible for visibility; your code doesn't have to be finished to create this! 64 | 65 | Once your work is complete, and all automated checks have passed, please mark your pull request as ready for review. Anyone is free to add a review, however before a pull request can be merged it will need approval from a project maintainer. 66 | 67 | Please tag at least one of [Sephsekla](https://github.com/Sephsekla), [erikyo](https://github.com/erikyo) or [johnhooks](https://github.com/johnhooks)to review. 68 | 69 | ### Merging a pull request 70 | 71 | Once your pull request is approved, it will be merged by a maintainer. Thank you for your contribution to the project! 72 | 73 | ## Releases 74 | 75 | New releases should only be created from the `Trunk` branch. This is handled by a GitHub action whenever a new tag is created on this branch. 76 | 77 | A new release should only be created by a project maintainer after discussion with the team. 78 | 79 | ## Meetings 80 | 81 | We hold weekly office hours at every Wednesday at 15:00 UTC in the [#feature-notifications](https://wordpress.slack.com/messages/C2K1C71FE) channel of the [Make WordPress Slack](https://make.wordpress.org/chat/). New contributors are always welcome! 82 | 83 | We also hold a monthly planning meeting via Google Meet. This is currently held on the last Tuesday of every month at 14:00 UTC. 84 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ( api ) => { 2 | api.cache( true ); 3 | 4 | return { 5 | presets: [ '@wordpress/babel-preset-default' ], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordpress/wp-feature-notifications", 3 | "description": "Notifications for WordPress (Feature Plugin)", 4 | "type": "wordpress-plugin", 5 | "require": { 6 | "ext-json": "*" 7 | }, 8 | "require-dev": { 9 | "phpunit/phpunit": "^9.6", 10 | "yoast/phpunit-polyfills": "^1.1", 11 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0", 12 | "wp-coding-standards/wpcs": "^2.3", 13 | "wp-phpunit/wp-phpunit": "^6.2", 14 | "squizlabs/php_codesniffer": "^3.7", 15 | "friendsofphp/php-cs-fixer": "3.16" 16 | }, 17 | "license": "GPL-2.0-or-later", 18 | "author": "The WordPress Contributors", 19 | "minimum-stability": "dev", 20 | "prefer-stable": true, 21 | "scripts": { 22 | "lint": "vendor/squizlabs/php_codesniffer/bin/phpcs includes/ -s --report=full,summary,source", 23 | "lint-fix": "vendor/bin/phpcbf --standard=phpcs.xml.dist includes/", 24 | "test": "vendor/bin/phpunit" 25 | }, 26 | "config": { 27 | "allow-plugins": { 28 | "dealerdirect/phpcodesniffer-composer-installer": true 29 | }, 30 | "platform": { 31 | "php": "7.4" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /includes/channels.php: -------------------------------------------------------------------------------- 1 | register( $name, $args ); 27 | } 28 | 29 | /** 30 | * Unregister a channel. 31 | * 32 | * @param string|Model\Channel $name Channel name including namespace, or 33 | * alternatively a complete Channel instance. 34 | * @return Model\Channel|false The unregistered channel on success, or false on failure. 35 | */ 36 | function unregister_channel( $name ) { 37 | return Channel_Registry::get_instance()->unregister( $name ); 38 | } 39 | 40 | // Register core notification channels. 41 | 42 | add_action( 43 | 'init', 44 | function () { 45 | register_channel( 46 | 'core/updates', 47 | array( 48 | 'title' => __( 'WordPress Updates', 'wp-feature-notifications' ), 49 | 'icon' => 'wordpress', 50 | 'description' => __( 'WordPress core update events.', 'wp-feature-notifications' ), 51 | ) 52 | ); 53 | 54 | register_channel( 55 | 'core/plugin-install', 56 | array( 57 | 'title' => __( 'Plugin Install', 'wp-feature-notifications' ), 58 | 'icon' => 'wordpress', 59 | 'description' => __( 'Plugin install events.', 'wp-feature-notifications' ), 60 | ) 61 | ); 62 | 63 | register_channel( 64 | 'core/plugin-uninstall', 65 | array( 66 | 'title' => __( 'Plugin Uninstall', 'wp-feature-notifications' ), 67 | 'icon' => 'wordpress', 68 | 'description' => __( 'Plugin uninstall events.', 'wp-feature-notifications' ), 69 | ) 70 | ); 71 | 72 | register_channel( 73 | 'core/plugin-activate', 74 | array( 75 | 'title' => __( 'Plugin Activate', 'wp-feature-notifications' ), 76 | 'icon' => 'wordpress', 77 | 'description' => __( 'Plugin activation events.', 'wp-feature-notifications' ), 78 | ) 79 | ); 80 | 81 | register_channel( 82 | 'core/plugin-deactivate', 83 | array( 84 | 'title' => __( 'Plugin Deactivate', 'wp-feature-notifications' ), 85 | 'icon' => 'wordpress', 86 | 'description' => __( 'Plugin deactivation events.', 'wp-feature-notifications' ), 87 | ) 88 | ); 89 | 90 | register_channel( 91 | 'core/plugin-updates', 92 | array( 93 | 'title' => __( 'Plugin Update', 'wp-feature-notifications' ), 94 | 'icon' => 'wordpress', 95 | 'description' => __( 'Plugin update events.', 'wp-feature-notifications' ), 96 | ) 97 | ); 98 | 99 | register_channel( 100 | 'core/post-new', 101 | array( 102 | 'title' => __( 'New Post', 'wp-feature-notifications' ), 103 | 'icon' => 'wordpress', 104 | 'description' => __( 'Post creation events.', 'wp-feature-notifications' ), 105 | ) 106 | ); 107 | 108 | register_channel( 109 | 'core/post-edit', 110 | array( 111 | 'title' => __( 'Edit Post', 'wp-feature-notifications' ), 112 | 'icon' => 'wordpress', 113 | 'description' => __( 'Post edit events.', 'wp-feature-notifications' ), 114 | ) 115 | ); 116 | 117 | register_channel( 118 | 'core/post-delete', 119 | array( 120 | 'title' => __( 'Delete Post', 'wp-feature-notifications' ), 121 | 'icon' => 'wordpress', 122 | 'description' => __( 'Post delete events.', 'wp-feature-notifications' ), 123 | ) 124 | ); 125 | 126 | register_channel( 127 | 'core/comment-new', 128 | array( 129 | 'title' => __( 'New Comment', 'wp-feature-notifications' ), 130 | 'icon' => 'wordpress', 131 | 'description' => __( 'Comment creation events.', 'wp-feature-notifications' ), 132 | ) 133 | ); 134 | } 135 | ); 136 | -------------------------------------------------------------------------------- /includes/class-activator.php: -------------------------------------------------------------------------------- 1 | WP_FEATURE_NOTIFICATION_PLUGIN_VERSION, 35 | 'max_lifespan' => 1000 * 60 * 60 * 24 * 31 * 6, // 6 months 36 | 'delete_on_dismiss' => false, 37 | ); 38 | } 39 | 40 | /** 41 | * Create or Update the WP_Notifications options. 42 | * 43 | * @return void 44 | */ 45 | public static function update_options() { 46 | 47 | self::init_options(); 48 | 49 | $options = get_option( 'wp_notifications_options' ); 50 | 51 | if ( false !== $options ) { 52 | 53 | // Update the plugin options but add the new options automatically 54 | if ( isset( $options['version'] ) ) { 55 | unset( $options['version'] ); 56 | } 57 | 58 | // Merge previous options, preserve the previously modified options as default. 59 | $new_options = array_merge( self::$default_options, $options ); 60 | 61 | update_option( 'wp_notifications_options', $new_options ); 62 | } else { 63 | // If the plugin options are missing, initialize the plugin with the default options. 64 | $new_options = array_merge( self::$default_options ); 65 | 66 | add_option( 'wp_notifications_options', $new_options ); 67 | } 68 | } 69 | 70 | /** 71 | * Install the WP Notifications plugin. 72 | * 73 | * Create the plugin's database tables and options 74 | * 75 | * @return void 76 | */ 77 | public static function install() { 78 | global $wpdb; 79 | 80 | // Engage multisite if in the middle of turning it on from network.php. 81 | $is_multisite = is_multisite() || ( defined( 'WP_INSTALLING_NETWORK' ) && WP_INSTALLING_NETWORK ); 82 | 83 | if ( $is_multisite ) { 84 | // Get all blogs in the network and uninstall the plugin on each one. 85 | $blog_ids = $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" ); 86 | 87 | // Loop over the individual sites and create tables for each. 88 | foreach ( $blog_ids as $blog_id ) { 89 | switch_to_blog( $blog_id ); 90 | 91 | self::create_tables(); 92 | 93 | restore_current_blog(); 94 | } 95 | } 96 | 97 | // Always create the main site database tables and options. 98 | self::create_tables(); 99 | } 100 | 101 | /** 102 | * Activate the WP Notifications plugin. 103 | * 104 | * @return void 105 | */ 106 | public static function activate() { 107 | self::install(); 108 | set_transient( 'wp_notifications_activation', true ); 109 | } 110 | 111 | /** 112 | * Create the WP Notifications tables and options. 113 | * 114 | * @return void 115 | */ 116 | public static function create_tables() { 117 | $db_version = get_option( 'wp_notifications_db_version' ); 118 | 119 | if ( ! $db_version ) { 120 | self::create_tables_v1(); 121 | update_option( 'wp_notifications_db_version', WP_FEATURE_NOTIFICATION_DB_VERSION ); 122 | } 123 | 124 | // If the options do not exist then create them 125 | self::update_options(); 126 | } 127 | 128 | /** 129 | * Create v1 WP Notifications tables and options. 130 | * 131 | * @return void 132 | */ 133 | public static function create_tables_v1() { 134 | global $wpdb; 135 | 136 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 137 | require_once WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/includes/database/class-schema.php'; 138 | 139 | $tables = $wpdb->get_results( 'SHOW TABLES' ); 140 | 141 | // Create the messages table 142 | if ( ! in_array( $wpdb->prefix . 'notifications_messages', $tables, true ) ) { 143 | $messages_sql = Database\Schema::messages_table_v1(); 144 | dbDelta( $messages_sql ); 145 | } 146 | 147 | // Create the subscriptions table 148 | if ( ! in_array( $wpdb->prefix . 'notifications_subscriptions', $tables, true ) ) { 149 | $subscriptions_sql = Database\Schema::subscriptions_table_v1(); 150 | dbDelta( $subscriptions_sql ); 151 | } 152 | 153 | // Create the queue table 154 | if ( ! in_array( $wpdb->prefix . 'notifications_queue', $tables, true ) ) { 155 | $queue_sql = Database\Schema::queue_table_v1(); 156 | dbDelta( $queue_sql ); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /includes/class-uninstaller.php: -------------------------------------------------------------------------------- 1 | get_col( "SELECT blog_id FROM $wpdb->blogs" ); 49 | 50 | foreach ( $blog_ids as $blog_id ) { 51 | switch_to_blog( $blog_id ); 52 | 53 | self::drop_tables(); 54 | self::delete_options(); 55 | 56 | restore_current_blog(); 57 | } 58 | } 59 | 60 | // Always remove the main site database tables and options. 61 | self::drop_tables(); 62 | self::delete_options(); 63 | } 64 | 65 | /** 66 | * Drop the WP Notifications database tables. 67 | * 68 | * @return void 69 | */ 70 | public static function drop_tables() { 71 | global $wpdb; 72 | 73 | $wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'notifications_messages' ); 74 | $wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'notifications_subscriptions' ); 75 | $wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'notifications_queue' ); 76 | } 77 | 78 | /** 79 | * Delete the WP Notifications options. 80 | * 81 | * @return void 82 | */ 83 | public static function delete_options() { 84 | delete_option( 'wp_notifications_db_version' ); 85 | delete_option( 'wp_notifications_options' ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /includes/database/class-schema.php: -------------------------------------------------------------------------------- 1 | get_charset_collate(); 21 | 22 | return 'CREATE TABLE `' . $wpdb->prefix . "notifications_messages` ( 23 | `id` BIGINT(20) NOT NULL, 24 | `channel_name` VARCHAR(50) NOT NULL, 25 | `channel_title` TINYTEXT NOT NULL, 26 | `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(), 27 | `expires_at` DATETIME NULL, 28 | `severity` VARCHAR(16) NULL, 29 | `title` TINYTEXT NULL, 30 | `message` TINYTEXT NULL, 31 | `meta` TEXT NULL, 32 | PRIMARY KEY (`id`), 33 | KEY `channel_name` (`channel_name`) 34 | ) $charset_collate;\n"; 35 | } 36 | 37 | 38 | public static function subscriptions_table_v1() { 39 | global $wpdb; 40 | 41 | $charset_collate = $wpdb->get_charset_collate(); 42 | 43 | return 'CREATE TABLE `' . $wpdb->prefix . "notifications_subscriptions` ( 44 | `user_id` BIGINT(20) NOT NULL, 45 | `channel_name` VARCHAR(50) NOT NULL, 46 | `snoozed_until` DATETIME NULL, 47 | KEY `user_id` (`user_id`), 48 | KEY `channel_name` (`channel_name`) 49 | ) $charset_collate;\n"; 50 | } 51 | 52 | public static function queue_table_v1() { 53 | global $wpdb; 54 | 55 | $charset_collate = $wpdb->get_charset_collate(); 56 | 57 | return 'CREATE TABLE `' . $wpdb->prefix . "notifications_queue` ( 58 | `message_id` BIGINT(20) NOT NULL, 59 | `user_id` BIGINT(20) NOT NULL, 60 | `dismissed_at` DATETIME NULL, 61 | `displayed_at` DATETIME NULL, 62 | KEY `message_id` (`message_id`), 63 | KEY `user_id` (`user_id`) 64 | ) $charset_collate;\n"; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /includes/exceptions/class-runtime-exception.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Message extends Framework\Factory { 20 | 21 | /** 22 | * Instantiates a Message object. 23 | * 24 | * @param array|string $args { 25 | * Array or string of arguments for creating a message. Supported arguments 26 | * are described below. 27 | * 28 | * @type ?string $message Text content of the message. 29 | * @type ?string $accept_label Optional label of the accept action. 30 | * @type ?string $accept_link Optional url of the accept action. 31 | * @type ?string $accept_message Optional label of the accept action. 32 | * @type ?string $channel_title Optional human-readable title of the channel 33 | * the message was emitted from. 34 | * @type ?DateTime $created_at Optional datetime at which a message was created. 35 | * Default `'null'` 36 | * @type ?string $dismiss_label Optional label of the dismiss action. 37 | * @type ?DateTime $expires_at Optional datetime at which a message expires. 38 | * Default `'null'` 39 | * @type ?string $icon Optional icon of the message. Default `null` 40 | * @type ?int $id Optional database ID of the message. Default `null` 41 | * @type ?bool $is_dismissible Optional boolean of whether the notice can 42 | * be dismissed. Default `true` 43 | * @type ?string $severity Optional severity of the message. Default `null` 44 | * @type string $title Optional human-readable message label. Default `''` 45 | * } 46 | * 47 | * @return Model\Message A newly created instance of Message or false. 48 | */ 49 | public function make( $args = array() ): Model\Message { 50 | $parsed = wp_parse_args( $args ); 51 | 52 | // Required properties 53 | 54 | $message = array_key_exists( 'message', $parsed ) ? $parsed['message'] : null; 55 | 56 | // Optional properties 57 | 58 | $accept_label = array_key_exists( 'accept_label', $parsed ) ? $parsed['accept_label'] : null; 59 | $accept_link = array_key_exists( 'accept_link', $parsed ) ? $parsed['accept_link'] : null; 60 | $channel_title = array_key_exists( 'channel_title', $parsed ) ? $parsed['channel_title'] : null; 61 | $created_at = array_key_exists( 'created_at', $parsed ) ? $parsed['created_at'] : null; 62 | $dismiss_label = array_key_exists( 'dismiss_label', $parsed ) ? $parsed['dismiss_label'] : null; 63 | $expires_at = array_key_exists( 'expires_at', $parsed ) ? $parsed['expires_at'] : null; 64 | $icon = array_key_exists( 'icon', $parsed ) ? $parsed['icon'] : null; 65 | $id = array_key_exists( 'id', $parsed ) ? $parsed['id'] : null; 66 | $is_dismissible = array_key_exists( 'is_dismissible', $parsed ) ? $parsed['is_dismissible'] : true; 67 | $severity = array_key_exists( 'severity', $parsed ) ? $parsed['severity'] : null; 68 | $title = array_key_exists( 'title', $parsed ) ? $parsed['title'] : ''; 69 | 70 | return new Model\Message( 71 | $message, 72 | $accept_label, 73 | $accept_link, 74 | $channel_title, 75 | $created_at, 76 | $dismiss_label, 77 | $expires_at, 78 | $icon, 79 | $id, 80 | $is_dismissible, 81 | $severity, 82 | $title, 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /includes/factory/class-notification.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class Notification extends Framework\Factory { 21 | 22 | /** 23 | * Instantiates a Notification object. 24 | 25 | * @param array|string $args { 26 | * Array or string of arguments for creating a notification. Supported arguments are described below. 27 | * 28 | * @type ?string $channel_name Channel name, including namespace, 29 | * the notification was emitted from. 30 | * @type ?int $message_id ID of the message related to the 31 | * notification. 32 | * @type ?int $user_id ID of the user the notification 33 | * belongs to. 34 | * @type ?string $context Optional display context of the 35 | * notification. Default `'adminbar'` 36 | * @type string|DateTime|null $created_at Optional datetime at which the 37 | * notification was created. Default `null` 38 | * @type string|DateTime|null $dismissed_at Optional datetime t which the 39 | * notification was dismissed. Default `null` 40 | * @type string|DateTime|null $displayed_at Optional datetime at which the 41 | * notification was first displayed. 42 | * Default `null` 43 | * @type string|DateTime|null $expires_at Optional datetime at which the 44 | * notification expires. Default `null` 45 | * } 46 | * 47 | * @return Model\Notification A newly created instance of Channel or false. 48 | */ 49 | public function make( $args = array() ): Model\Notification { 50 | $parsed = wp_parse_args( $args ); 51 | 52 | // Required properties 53 | 54 | $channel_name = array_key_exists( 'channel_name', $parsed ) ? $parsed['channel_name'] : null; 55 | $message_id = array_key_exists( 'message_id', $parsed ) ? $parsed['message_id'] : null; 56 | $user_id = array_key_exists( 'user_id', $parsed ) ? $parsed['user_id'] : null; 57 | 58 | // Optional properties 59 | 60 | $context = array_key_exists( 'context', $parsed ) ? $parsed['context'] : 'adminbar'; 61 | $created_at = array_key_exists( 'created_at', $parsed ) ? $parsed['created_at'] : null; 62 | $dismissed_at = array_key_exists( 'dismissed_at', $parsed ) ? $parsed['dismissed_at'] : null; 63 | $displayed_at = array_key_exists( 'displayed_at', $parsed ) ? $parsed['displayed_at'] : null; 64 | $expires_at = array_key_exists( 'expires_at', $parsed ) ? $parsed['expires_at'] : null; 65 | 66 | // Deserialize MySQL datetime strings. 67 | 68 | $created_at = Helper\Serde::maybe_deserialize_mysql_date( $created_at ); 69 | $dismissed_at = Helper\Serde::maybe_deserialize_mysql_date( $dismissed_at ); 70 | $displayed_at = Helper\Serde::maybe_deserialize_mysql_date( $displayed_at ); 71 | $expires_at = Helper\Serde::maybe_deserialize_mysql_date( $expires_at ); 72 | 73 | return new Model\Notification( 74 | $channel_name, 75 | $message_id, 76 | $user_id, 77 | $context, 78 | $created_at, 79 | $dismissed_at, 80 | $displayed_at, 81 | $expires_at 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /includes/factory/class-subscription.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | class Subscription extends Framework\Factory { 21 | 22 | /** 23 | * Instantiates a Subscription object. 24 | * 25 | * @param array|string $args { 26 | * Array or string of arguments for creating a subscription. Supported 27 | * arguments are described below. 28 | * 29 | * @type ?string $channel_name Namespaced channel name of the 30 | * subscription. 31 | * @type ?int $user_id ID of the user the subscription 32 | * belongs to. 33 | * @type string|DateTime|null $created_at Optional datetime at which the 34 | * subscription was created. 35 | * @type string|DateTime|null $snoozed_until Optional snoozed until datetime 36 | * of the subscription. 37 | * } 38 | * 39 | * @return Model\Subscription A newly created instance of Subscription or false. 40 | */ 41 | public function make( $args = array() ): Model\Subscription { 42 | $parsed = wp_parse_args( $args ); 43 | 44 | // Required properties 45 | 46 | $channel_name = array_key_exists( 'channel_name', $parsed ) ? $parsed['channel_name'] : null; 47 | $user_id = array_key_exists( 'user_id', $parsed ) ? $parsed['user_id'] : null; 48 | 49 | // Optional properties 50 | 51 | $created_at = array_key_exists( 'created_at', $parsed ) ? $parsed['created_at'] : null; 52 | $snoozed_until = array_key_exists( 'snoozed_until', $parsed ) ? $parsed['snoozed_until'] : null; 53 | 54 | // Deserialize MySQL datetime strings. 55 | 56 | $created_at = Helper\Serde::maybe_deserialize_mysql_date( $created_at ); 57 | $snoozed_until = Helper\Serde::maybe_deserialize_mysql_date( $snoozed_until ); 58 | 59 | return new Model\Subscription( 60 | $channel_name, 61 | $user_id, 62 | $created_at, 63 | $snoozed_until, 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /includes/framework/class-factory.php: -------------------------------------------------------------------------------- 1 | format( DateTime::ATOM ); 32 | } 33 | 34 | /** 35 | * Maybe deserialize a datetime string in MySQL format. 36 | * 37 | * @param string|DateTime|null $date The possible MySQL datetime to deserialize. 38 | * 39 | * @return DateTime|null Maybe a DateTime object. 40 | */ 41 | public static function maybe_deserialize_mysql_date( $date ) { 42 | if ( null === $date ) { 43 | return null; 44 | } 45 | 46 | if ( $date instanceof DateTime ) { 47 | return $date; 48 | } 49 | 50 | if ( is_string( $date ) ) { 51 | $date = DateTime::createFromFormat( 'Y-m-d H:i:s', $date ); 52 | 53 | if ( false === $date ) { 54 | $date = null; 55 | } 56 | } 57 | 58 | return $date; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /includes/image/class-base-image.php: -------------------------------------------------------------------------------- 1 | html node 11 | */ 12 | protected $source; 13 | 14 | /** 15 | * @var string for alternative text of an html node 16 | */ 17 | protected $alt = ''; 18 | 19 | /** 20 | * BaseImage constructor. 21 | * 22 | * @param string $source 23 | * @param string $alt 24 | */ 25 | public function __construct( $source, $alt = '' ) { 26 | $this->source = $source; 27 | $this->alt = $alt; 28 | } 29 | 30 | /** 31 | * @return array 32 | */ 33 | public function jsonSerialize() { 34 | 35 | $data = array(); 36 | 37 | if ( ! empty( $this->get_source() ) ) { 38 | $data['source'] = $this->get_source(); 39 | } 40 | 41 | if ( ! empty( $this->get_alt() ) ) { 42 | $data['alt'] = $this->get_alt(); 43 | } 44 | 45 | return $data; 46 | } 47 | 48 | /** 49 | * Source of the image to be used on src attribute of 50 | * 51 | * @return string 52 | */ 53 | public function get_source() { 54 | return $this->source; 55 | } 56 | 57 | /** 58 | * Alternative text to be used on alt attribute of 59 | * 60 | * @return string 61 | */ 62 | public function get_alt() { 63 | return $this->alt; 64 | } 65 | 66 | /** 67 | * Instantiates a BaseImage based on a JSON string 68 | * 69 | * @param string $json 70 | * 71 | * @return Json_Unserializable 72 | */ 73 | public static function json_unserialize( $json ) { 74 | 75 | $data = json_decode( $json, true ); 76 | 77 | $source = ! empty( $data['source'] ) ? $data['source'] : ''; 78 | $alt = ! empty( $data['alt'] ) ? $data['alt'] : ''; 79 | 80 | return new self( $source, $alt ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /includes/image/interface-image.php: -------------------------------------------------------------------------------- 1 | register_routes(); 36 | $notification_controller->register_routes(); 37 | $subscription_controller->register_routes(); 38 | 39 | } 40 | 41 | add_action( 'rest_api_init', '\WP\Notifications\register_routes' ); 42 | 43 | /** 44 | * Adds WP Notifications icon after the user avatar in the top admin bar in the "secondary" position 45 | * 46 | * @param WP_Admin_Bar $wp_admin_bar Toolbar instance. 47 | */ 48 | function admin_bar_item( WP_Admin_Bar $wp_admin_bar ) { 49 | if ( ! is_user_logged_in() ) { 50 | return; 51 | } 52 | 53 | /** 54 | * This is the same HTML as the `src/scripts/components/NotificationHub.js` 55 | * If this is changed that file must also be updated. 56 | */ 57 | $notification_hub = sprintf( 58 | '', 59 | __( 'Notifications', 'wp-feature-notifications' ) 60 | ); 61 | 62 | /** 63 | * This is the same HTML as the `src/scripts/components/NotificationHubIcon.js` 64 | * If this is changed that file must also be updated. 65 | */ 66 | $notification_hub_icon = sprintf( 67 | '
', 68 | __( 'Notifications', 'wp-feature-notifications' ) 69 | ); 70 | 71 | $args = array( 72 | 'id' => 'wp-notifications-hub', 73 | 'parent' => 'top-secondary', 74 | 'title' => $notification_hub_icon, 75 | 'meta' => array( 76 | 'tabindex' => 0, 77 | 'html' => $notification_hub, 78 | ), 79 | ); 80 | $wp_admin_bar->add_node( $args ); 81 | } 82 | 83 | add_action( 'admin_bar_menu', '\WP\Notifications\admin_bar_item', 1 ); 84 | 85 | /** 86 | * Register and enqueue a wp-notifications scripts and stylesheet in WordPress admin. 87 | */ 88 | function enqueue_admin_assets() { 89 | if ( ! is_user_logged_in() ) { 90 | return; 91 | } 92 | 93 | /* Load styles */ 94 | wp_register_style( 'wp_notifications', WP_FEATURE_NOTIFICATION_PLUGIN_DIR_URL . '/build/wp-notifications.css', array(), WP_FEATURE_NOTIFICATION_PLUGIN_VERSION ); 95 | wp_enqueue_style( 'wp_notifications' ); 96 | 97 | /* Load scripts */ 98 | $asset = include WP_FEATURE_NOTIFICATION_PLUGIN_DIR . '/build/wp-notifications.asset.php'; 99 | wp_register_script( 'wp_notifications', WP_FEATURE_NOTIFICATION_PLUGIN_DIR_URL . '/build/wp-notifications.js', $asset['dependencies'], WP_FEATURE_NOTIFICATION_PLUGIN_VERSION, true ); 100 | wp_enqueue_script( 'wp_notifications' ); 101 | } 102 | 103 | 104 | add_action( 'wp_enqueue_scripts', '\WP\Notifications\enqueue_admin_assets', 0 ); 105 | add_action( 'admin_enqueue_scripts', '\WP\Notifications\enqueue_admin_assets', 0 ); 106 | -------------------------------------------------------------------------------- /includes/model/class-channel.php: -------------------------------------------------------------------------------- 1 | name = $name; 72 | $this->title = $title; 73 | 74 | // Optional properties 75 | 76 | $this->context = $context; 77 | $this->icon = $icon; 78 | $this->description = $description; 79 | } 80 | 81 | /** 82 | * Specifies data which should be serialized to JSON. 83 | * 84 | * @return array Data which can be serialized by json_encode, which is a 85 | * value of any type other than a resource. 86 | */ 87 | public function jsonSerialize() { 88 | return array( 89 | 'context' => $this->context, 90 | 'description' => $this->description, 91 | 'icon' => $this->icon, 92 | 'name' => $this->name, 93 | 'title' => $this->title, 94 | ); 95 | } 96 | 97 | /** 98 | * Get the namespaced name. 99 | * 100 | * @return ?string The namespaced name of the channel. 101 | */ 102 | public function get_name(): ?string { 103 | return $this->name; 104 | } 105 | 106 | /** 107 | * Get the human-readable label. 108 | * 109 | * @return ?string The title of the channel. 110 | */ 111 | public function get_title(): ?string { 112 | return $this->title; 113 | } 114 | 115 | /** 116 | * Get the default display context. 117 | * 118 | * @return ?string The context of the channel. 119 | */ 120 | public function get_context(): ?string { 121 | return $this->context; 122 | } 123 | 124 | /** 125 | * Get the detailed description. 126 | * 127 | * @return ?string The description of the channel. 128 | */ 129 | public function get_description(): ?string { 130 | return $this->description; 131 | } 132 | 133 | /** 134 | * Get the icon. 135 | * 136 | * @return ?string The icon of the channel. 137 | */ 138 | public function get_icon(): ?string { 139 | return $this->icon; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /includes/model/class-subscription.php: -------------------------------------------------------------------------------- 1 | channel_name = $channel_name; 68 | $this->user_id = $user_id; 69 | 70 | // Optional properties 71 | 72 | $this->created_at = $created_at; 73 | $this->snoozed_until = $snoozed_until; 74 | } 75 | 76 | /** 77 | * Specifies data which should be serialized to JSON. 78 | * 79 | * @return array Data which can be serialized by json_encode, which is a 80 | * value of any type other than a resource. 81 | */ 82 | public function jsonSerialize() { 83 | return array( 84 | 'channel_name' => $this->channel_name, 85 | 'created_at' => Helper\Serde::maybe_serialize_json_date( $this->created_at ), 86 | 'snoozed_until' => Helper\Serde::maybe_serialize_json_date( $this->snoozed_until ), 87 | 'user_id' => $this->user_id, 88 | ); 89 | } 90 | 91 | /** 92 | * Get the namespaced channel name. 93 | * 94 | * @return ?string The namespaced channel name of the subscription. 95 | */ 96 | public function get_channel_name(): ?string { 97 | return $this->channel_name; 98 | } 99 | 100 | /** 101 | * Get the created at datetime. 102 | * 103 | * @return ?DateTime The datetime at which the subscription was created. 104 | */ 105 | public function get_created_at(): ?DateTime { 106 | return $this->created_at; 107 | } 108 | 109 | /** 110 | * Get the snoozed until option. 111 | * 112 | * @return ?DateTime The snoozed until option of the subscription. 113 | */ 114 | public function get_snoozed_until(): ?DateTime { 115 | return $this->snoozed_until; 116 | } 117 | 118 | /** 119 | * Get the user ID. 120 | * 121 | * @return ?int The user ID of the subscription. 122 | */ 123 | public function get_user_id(): ?int { 124 | return $this->user_id; 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /includes/persistence/class-abstract-notification-repository.php: -------------------------------------------------------------------------------- 1 | sub( $interval ); 32 | 33 | return $this->find_by_date_range( 34 | $start, 35 | $end, 36 | Order::DESCENDING, 37 | $pagination, 38 | $offset 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /includes/persistence/class-wpdb-notification-repository.php: -------------------------------------------------------------------------------- 1 | WP_REST_Server::READABLE, 49 | 'args' => $this->get_collection_params(), 50 | 'callback' => array( $this, 'get_items' ), 51 | 'permission_callback' => array( $this, 'get_items_permissions_check' ), 52 | ), 53 | 'schema' => array( $this, 'get_public_item_schema' ), 54 | ), 55 | false 56 | ); 57 | } 58 | 59 | /** 60 | * Checks if a given request has access to view channels. 61 | * 62 | * @param WP_REST_Request $request Full details about the request. 63 | * 64 | * @return true|WP_Error True if the request has access to view the items, error object otherwise. 65 | */ 66 | public function get_items_permissions_check( $request ) { 67 | if ( ! is_user_logged_in() ) { 68 | return new WP_Error( 69 | 'rest_notifications_login_required', 70 | __( 'Sorry, you must be logged to view channels.' ), 71 | array( 'status' => 401 ) 72 | ); 73 | } 74 | return true; 75 | } 76 | 77 | /** 78 | * Get channels for request. 79 | * 80 | * @param WP_REST_Request $request Received REST request 81 | * 82 | * @return WP_REST_RESPONSE|WP_Error REST response or WP Error 83 | */ 84 | public function get_items( $request ) { 85 | $channels = Channel_Registry::get_instance()->get_all_registered(); 86 | 87 | // TODO filter based on permissions. 88 | 89 | return rest_ensure_response( $channels ); 90 | } 91 | 92 | /** 93 | * Retrieves the channels' schema, conforming to JSON Schema. 94 | * 95 | * @return array The notification channel schema. 96 | */ 97 | public function get_item_schema(): array { 98 | if ( $this->schema ) { 99 | return $this->add_additional_fields_schema( $this->schema ); 100 | } 101 | 102 | $schema = array( 103 | '$schema' => 'http://json-schema.org/draft-04/schema#', 104 | 'title' => 'channel', 105 | 'type' => 'object', 106 | 'properties' => array( 107 | 'context' => array( 108 | 'description' => __( 'The default view context the notification channel.' ), 109 | 'type' => 'string', 110 | 'context' => array( 'view', 'embed' ), 111 | 'enum' => array( 112 | 'adminbar', 113 | 'dashboard', 114 | ), 115 | 'readonly' => true, 116 | ), 117 | 'icon' => array( 118 | 'description' => __( 'The default icon of the notification channel.' ), 119 | 'type' => 'integer', 120 | 'context' => array( 'view', 'embed' ), 121 | 'readonly' => true, 122 | ), 123 | 'name' => array( 124 | 'description' => __( 'Unique identifier for the notification channel.' ), 125 | 'type' => 'string', 126 | 'context' => array( 'view', 'embed' ), 127 | 'readonly' => true, 128 | ), 129 | 'title' => array( 130 | 'description' => __( 'The human-readable label of the notification channel.' ), 131 | 'type' => 'string', 132 | 'context' => array( 'view', 'embed' ), 133 | 'readonly' => true, 134 | ), 135 | ), 136 | ); 137 | 138 | // Cache generated schema on endpoint instance. 139 | $this->schema = $schema; 140 | 141 | return $this->add_additional_fields_schema( $this->schema ); 142 | } 143 | 144 | /** 145 | * Retrieves the query params for collections. 146 | * 147 | * @return array Channel collection parameters. 148 | */ 149 | public function get_collection_params(): array { 150 | $query_params = parent::get_collection_params(); 151 | 152 | $query_params['context']['default'] = 'view'; 153 | 154 | $query_params['offset'] = array( 155 | 'description' => __( 'Offset the result set by a specific number of items.' ), 156 | 'type' => 'integer', 157 | ); 158 | 159 | $query_params['context'] = array( 160 | 'description' => __( 'Limit result set to channels assigned a specific display context.' ), 161 | 'default' => 'all', 162 | 'type' => 'string', 163 | ); 164 | 165 | return $query_params; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const jestConfig = { 2 | verbose: true, 3 | preset: '@wordpress/jest-preset-default', 4 | setupFilesAfterEnv: [ 'expect-puppeteer' ], 5 | projects: [ 6 | { 7 | displayName: 'unit', 8 | testMatch: [ '/tests/jsunit/**/*.test.js' ], 9 | }, 10 | { 11 | displayName: 'e2e', 12 | preset: 'jest-puppeteer', 13 | testMatch: [ '/tests/jse2e/**/*.test.js' ], 14 | }, 15 | ], 16 | }; 17 | 18 | module.exports = jestConfig; 19 | -------------------------------------------------------------------------------- /languages/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/wp-feature-notifications/ac990811d8c8ba4ba090fe99f71839991866b662/languages/.gitkeep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-feature-notifications", 3 | "version": "0.1.0", 4 | "description": "A notification center for WordPress.", 5 | "keywords": [ 6 | "WordPress", 7 | "notifications", 8 | "dashboard" 9 | ], 10 | "author": "The WordPress Contributors", 11 | "license": "GPL-2.0-or-later", 12 | "bugs": { 13 | "url": "https://github.com/WordPress/wp-feature-notifications/issues" 14 | }, 15 | "homepage": "https://github.com/WordPress/wp-feature-notifications#readme", 16 | "directories": { 17 | "doc": "docs", 18 | "test": "tests" 19 | }, 20 | "files": [ 21 | "build/*", 22 | "includes/*", 23 | "readme.txt", 24 | "wp-feature-notifications.php" 25 | ], 26 | "workspaces": [ 27 | "./storybook" 28 | ], 29 | "scripts": { 30 | "start": "wp-scripts start", 31 | "start:storybook": "npm run start --workspace=storybook", 32 | "build": "wp-scripts build", 33 | "build:storybook": "npm run build --workspace=storybook", 34 | "php-install": "wp-env run composer 'composer --ignore-platform-req=php install'", 35 | "packages-update": "wp-scripts packages-update", 36 | "check-engines": "wp-scripts check-engines", 37 | "check-licenses": "wp-scripts check-licenses", 38 | "format": "wp-scripts format", 39 | "lint:css": "wp-scripts lint-style ./src", 40 | "lint:js": "wp-scripts lint-js ./src", 41 | "lint:md:docs": "wp-scripts lint-md-docs ./src", 42 | "lint:pkg-json": "wp-scripts lint-pkg-json ./src", 43 | "plugin-zip": "wp-scripts plugin-zip", 44 | "test:js": "jest", 45 | "test:php": "wp-env run phpunit 'phpunit --configuration=/var/www/html/wp-content/plugins/wp-feature-notifications/phpunit.xml.dist'", 46 | "test": "npm run test:php && npm run test:js", 47 | "docGen": "npx docgen src/scripts/wp-notifications.js", 48 | "prepare": "husky install" 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "git+https://github.com/WordPress/wp-feature-notifications.git" 53 | }, 54 | "engines": { 55 | "node": ">=14", 56 | "npm": ">=7" 57 | }, 58 | "devDependencies": { 59 | "@types/jest": "^29.5.10", 60 | "@wordpress/api-fetch": "^6.44.0", 61 | "@wordpress/components": "^25.13.0", 62 | "@wordpress/data": "^9.17.0", 63 | "@wordpress/date": "^4.47.0", 64 | "@wordpress/e2e-test-utils": "^10.18.0", 65 | "@wordpress/env": "^8.13.0", 66 | "@wordpress/escape-html": "^2.47.0", 67 | "@wordpress/eslint-plugin": "^17.4.0", 68 | "@wordpress/i18n": "^4.47.0", 69 | "@wordpress/icons": "^9.38.0", 70 | "@wordpress/keyboard-shortcuts": "^4.24.0", 71 | "@wordpress/scripts": "^26.18.0", 72 | "classnames": "^2.3.2", 73 | "eslint-import-resolver-typescript": "^3.6.1", 74 | "husky": "^8.0.3", 75 | "jest-puppeteer": "^9.0.1", 76 | "lint-staged": "^15.1.0", 77 | "prettier": "npm:wp-prettier@^3.0.3", 78 | "re-resizable": "^6.9.11", 79 | "typescript": "^5.3.2" 80 | }, 81 | "browserslist": [ 82 | "extends @wordpress/browserslist-config" 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Apply WordPress Coding Standards to plugin and tests files 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | . 27 | 28 | 29 | 30 | 31 | 32 | 33 | warning 34 | 35 | 36 | warning 37 | 38 | 39 | warning 40 | 41 | 42 | warning 43 | 44 | 45 | 46 | 47 | 48 | /node_modules/* 49 | /vendor/* 50 | /build/* 51 | 52 | 53 | 54 | /tests/phpunit/tests/* 55 | 56 | 57 | /tests/phpunit/tests/* 58 | 59 | 60 | /tests/phpunit/tests/* 61 | 62 | 63 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | tests/phpunit/tests 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === WP Feature Notifications === 2 | Contributors: schlessera, psykro, raaaahman, danbilauca, sephsekla, bacoords, erikyo 3 | Tags: feature-notifications 4 | Requires at least: 6.2 5 | Tested up to: 6.2 6 | Requires PHP: 7.4 7 | License: GPLv2 or later 8 | License URI: https://www.gnu.org/licenses/gpl-2.0.html 9 | 10 | A new (better) way to manage and deliver WordPress notifications to the relevant audience. 11 | 12 | == Description == 13 | 14 | 1. Allow WordPress core to send notifications to administrative users to give them feedback about changes in the system. 15 | 1. Allow plugin and theme authors to send notifications to administrative users to give them feedback about changes in the system 16 | 1. Prevent plugin and theme authors from abusing this notification system in “spammy” ways. 17 | 1. Allow WordPress users who have access to notifications to control which, how, and where they receive them. 18 | 19 | Want to contribute? Checkout the code [on GitHub](https://github.com/WordPress/wp-feature-notifications) and read more about the project's goals on our [wiki](https://github.com/WordPress/wp-feature-notifications/wiki). 20 | 21 | == Screenshots == 22 | 23 | == Changelog == 24 | 25 | = 0.0.1 = 26 | * Initial Release 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/components/drawer/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from '@wordpress/element'; 2 | import { __ } from '@wordpress/i18n'; 3 | import { useShortcut } from '@wordpress/keyboard-shortcuts'; 4 | import { Resizable } from 're-resizable'; 5 | import type { FocusEventHandler, MutableRefObject } from 'react'; 6 | 7 | import { HUB_WIDTH } from '../../constants'; 8 | import NoticesArea from '../notice-area'; 9 | 10 | /** 11 | * The `Drawer` props type. 12 | */ 13 | type Props = { 14 | blur: FocusEventHandler< HTMLElement >; 15 | focus: FocusEventHandler< HTMLElement >; 16 | drawRef: MutableRefObject< HTMLElement >; 17 | }; 18 | 19 | /** 20 | * A resizable drawer React component for displaying notifications. The returned 21 | * `Drawer` component is an `aside` element containing a `Resizable` component and 22 | * `NoticesArea` component. The `Resizable` component provides UI controls to resize 23 | * the width of the `Drawer` by dragging its left edge. The `NoticesArea` component 24 | * displays notifications in the `Drawer`. 25 | * 26 | * @param props 27 | * @param props.focus The `onFocus` event listener for the drawer. 28 | * @param props.blur The `onBlur` event listener for the drawer. 29 | * @param props.drawRef The reference to the drawer element. 30 | */ 31 | export default function Drawer( { focus, blur, drawRef }: Props ) { 32 | /** 33 | * Enables the shortcut to close the drawer with the escape key 34 | */ 35 | useShortcut( 'wp-feature-notifications/close-drawer', () => blur ); 36 | const [ width, setWidth ] = useState( HUB_WIDTH ); 37 | 38 | return ( 39 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/hub-icon/index.tsx: -------------------------------------------------------------------------------- 1 | import { __ } from '@wordpress/i18n'; 2 | import { useShortcut } from '@wordpress/keyboard-shortcuts'; 3 | import classNames from 'classnames'; 4 | 5 | import UnreadDot from '../unread-dot'; 6 | 7 | /** 8 | * The HTML rendered by this component is the same as the variable `notification_hub_icon` 9 | * in`includes/load.php`. If the output of this component is modified that file must 10 | * also be updated. 11 | */ 12 | 13 | /** 14 | * The `HubIcon` props type. 15 | */ 16 | type Props = { 17 | classes?: string[]; 18 | hasUnread?: boolean; 19 | isActive: boolean; 20 | toggle: Function; 21 | }; 22 | 23 | /** 24 | * Notification icon UI component. 25 | * 26 | * @param props 27 | * @param props.toggle Toggle the drawer on and off. 28 | * @param props.isActive Predicate of whether the drawer is in an active state. 29 | * @param props.hasUnread Predicate of whether there are unread notifications. 30 | * @param props.classes The icon class names (defaults to [ 'dashicons-bell' ]) 31 | */ 32 | export default function HubIcon( { 33 | toggle, 34 | isActive, 35 | hasUnread = false, 36 | classes = [ 'dashicons-bell' ], 37 | }: Props ) { 38 | /** 39 | * Enables the shortcut to close the drawer with the escape key 40 | */ 41 | useShortcut( 'wp-feature-notifications/close-drawer', () => { 42 | if ( isActive ) toggle(); 43 | } ); 44 | 45 | return ( 46 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/notice-area/footer/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@wordpress/components'; 2 | import { __ } from '@wordpress/i18n'; 3 | import { cog } from '@wordpress/icons'; 4 | 5 | import { settingsPageUrl } from '../../../store/constants'; 6 | 7 | /** 8 | * The footer for the notices section drawer. 9 | * Has a button that links to the settings page. 10 | */ 11 | export default function NoticeAreaFooter() { 12 | return ( 13 |
14 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/notice-area/index.tsx: -------------------------------------------------------------------------------- 1 | import { useSelect } from '@wordpress/data'; 2 | import { __ } from '@wordpress/i18n'; 3 | 4 | import store from '../../store'; 5 | import { defaultContext } from '../../store/constants'; 6 | import type { Notice } from '../../types'; 7 | import { splitByDate } from '../../utils'; 8 | import NoticeEmpty from '../notice-empty'; 9 | import NoticesLoop from '../notice-loop'; 10 | 11 | import Footer from './footer'; 12 | import SectionHeader from './section-header'; 13 | 14 | /** 15 | * The `NoticeArea` props type. 16 | */ 17 | type Props = { 18 | context: string; 19 | notifications?: Notice[]; 20 | }; 21 | 22 | /** 23 | * WP Notification Feature toolbar in the secondary position of the admin bar 24 | * It watches for state updates and renders a component with the 25 | * updated state 26 | * 27 | * @param props 28 | * @param props.context Optional display context to render. 29 | * @param props.notifications The collection of notices to render. 30 | */ 31 | export default function NoticesArea( { 32 | context = defaultContext, 33 | notifications, 34 | }: Props ) { 35 | /* 36 | * Todo: this method should supply to rest api the user data, current page, moreover the request args may be added (notice per page, notice filters and sort) 37 | */ 38 | notifications = useSelect( 39 | ( select ) => select( store ).getNotices( context ), 40 | [ context ] 41 | ); 42 | 43 | /** 44 | * if the context is the adminbar we need to render a list of notifications with the recent notifications and the old notifications 45 | */ 46 | if ( context === defaultContext ) { 47 | /** Returns the empty notice banner whenever the number of notices is 0 */ 48 | if ( ! notifications?.length ) { 49 | return ; 50 | } 51 | 52 | /** split the notifications by date */ 53 | const sorted = splitByDate( notifications ); 54 | 55 | return ( 56 | <> 57 | { sorted.map( ( list, index ) => ( 58 |
64 | notice.status === 'new' 69 | ).length || 0 70 | } 71 | isMain={ index === 0 } // the main section is the first one 72 | /> 73 | 74 |
75 | ) ) } 76 |