strapi-plugin-reactions

2.0.1 • Public • Published
Logo - Strapi Reaction plugin

Strapi - Reactions plugin

All-in-One reactions plugin for any Content Type. Simple & flexible

GitHub package.json version Monthly download on NPM CircleCI codecov.io

UI preview

A plugin for Strapi Headless CMS that provides flexible & configurable reactions experience to any Content Types.

Table of Contents

  1. 💎 Versions
  2. ✨ Features
  3. ⏳ Installation
  4. 🖐 Requirements
  5. 🔧 Configuration
  6. 🕸️ Public API - REST
  7. 🕸️ Public API - GraphQL
  8. 🔌 Enrich service for Strapi extensions
  9. 💬 FAQ
  10. 🤝 Contributing
  11. 👨‍💻 Community support

💎 Versions

  • Strapi v5 - (current) v2.x
  • Strapi v4 - v1.x

✨ Features

  • Public REST & GraphQL API: Listing, setting, unsetting, toggling and much more via REST or GraphQL API. Easy to integrate with.
  • Any Content Type relation: Reactions can be used to any of your Content Types without any special configuration.
  • Emoji & Image reactions: You can define reaction types using predefined set of Emoji or use your own.
  • Content Manager Injection Zone: Making use of Strapi built-in batteries like Injection Zones to provide you highers user experience. Visual representation of reactions counter for any Content Types - useful!
  • Developer Experience boosted: A dedicated Enrich service provided to let you extend your Content API controllers by a single line to get reactions per each!

Request a feature by raising an issue.

⏳ Installation

Prerequisites

Database connection update

Because of Emoji usage, you will need (or maybe you already did it because of earlier requirements) to extend the database connection section in your Strapi project configuration.

// config/database.ts
// config/<env>/database.ts

connection: {
    charset: 'utf8mb4',
    collation: 'utf8mb4_unicode_ci',
    // your database credentials
}

Based on this specific charset, your emoji reactions are going to be saved in the database like a charm. See the reference issue raised on Strapi repository.

Via command line

(Use yarn to install this plugin within your Strapi project (recommended). Install yarn with these docs.)

yarn add strapi-plugin-reactions@latest

After successful installation you've to re-build your Strapi instance. To archive that simply use:

yarn build
yarn develop

or just run Strapi in the development mode with --watch-admin option:

yarn develop --watch-admin

The Reactions plugin should appear in the Settings section of Strapi after you run app again.

As a next step you must configure your the plugin by adding types of reactions you want to use. See Configuration section.

All done. Enjoy 🎉

🖐 Requirements

Complete installation requirements are exact same as for Strapi itself and can be found in the documentation under Installation Requirements.

Minimum environment requirements

  • Node.js >=18.0.0 <=20.x.x
  • NPM >=6.x.x

In our minimum support we're following official Node.js releases timelines.

Supported Strapi versions:

  • Strapi v5.3.0 (recently tested)
  • Strapi v5.x

Plugin dependencies

  • @strapi/plugin-graphql - required to run because built-in support for GraphQL handled by this plugin

We recommend always using the latest version of Strapi to start your new projects.

🔧 Configuration

To start your journey with Reactions plugin you must first setup types of reactions using the dedicated Settings page.

There is no need to provide any specific changed in the plugin configuration files extept enabling it.

export default () => ({
  //...
  reactions: {
    enabled: true,
  },
  //...
});

👤 RBAC

Plugin provides granular permissions based on Strapi RBAC functionality.

Mandatory permissions

For any role different than Super Admin, to access the Reactions settings you must set following permissions:

  • Plugins -> Reactions -> Reactions: Read - gives you the basic read access to Reactions settings
  • Plugins -> Reactions -> Reactions: Change - you're able to change the configuration of plugin
  • Plugins -> Reactions -> Reactions: Admin - you're able to perform administrator action in the global scope of a plugin

Data models

Reaction kind / type

{
  "documentId": "njx99iv4p4txuqp307ye8625",
  "name": "Like",
  "slug": "like",
  "emoji": "👍",
  "emojiFallbackUrl": "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f44d.png",
  "createdAt": "2023-09-14T20:13:01.649Z",
  "updatedAt": "2023-09-14T20:13:01.670Z",
  "icon": null
}

Reaction

{
  "documentId": "njx99iv4p4txuqp307ye8625",
  "kind": { // Type of reaction, not provided when listing by exact kind
    "documentId": "njx99iv4p4txuqp307ye8625",
    "slug": "like",
    "name": "Like" 
  },
  "user": { // User who trigger reaction, not provided when listing by exact user
    "documentId": "njx99iv4p4txuqp307ye8625",
    "username": "Joe Doe",
    "email": "jdoe@sample.com",
  },
  "createdAt": "2023-09-14T20:13:01.649Z",
  "updatedAt": "2023-09-14T20:13:01.670Z",
}

🕸️ Public REST API specification

Get reaction kinds / types

GraphQL equivalent: Public GraphQL API -> Get reaction kinds / types

GET <host>/api/reactions/kinds

Return a list of available reaction kinds to use on the end user interface and expose for interaction with users.

Example URL: https://localhost:1337/api/reactions/kinds

Example response body

[
  {
  "documentId": "njx99iv4p4txuqp307ye8625",
  "name": "Like",
  "slug": "like",
  "emoji": "👍",
  "emojiFallbackUrl": "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f44d.png",
  "createdAt": "2023-09-14T20:13:01.649Z",
  "updatedAt": "2023-09-14T20:13:01.670Z",
  "icon": null
  },
  // ...
]

List all reactions associated with Content Type

GraphQL equivalent: Public GraphQL API -> List all reactions associated with Content Type

GET <host>/api/reactions/list/single/<single type UID>?locale=<locale code> GET <host>/api/reactions/list/collection/<collection type UID>/<documentId>?locale=<locale code>

Return all reactions assiciated with provided Collection / Single Type UID and Content Type Document ID with following combinations:

  • all - if you're not providing the user context via Authorization header
  • all related with user - if call is done with user context via Authorization header

Example URL: https://localhost:1337/api/reactions/list/single/api::homepage.homepage?locale=en Example URL: https://localhost:1337/api/reactions/list/collection/api::post.post/njx99iv4p4txuqp307ye8625?locale=en

Example response body

[
  {
    "documentId": "njx99iv4p4txuqp307ye8625",
    "createdAt": "2023-09-14T20:13:01.649Z",
    "updatedAt": "2023-09-14T20:13:01.670Z",
    "kind":{
      "documentId": "njx99iv4p4txuqp307ye8625",
      "slug": "like",
      "name": "Like"
    },
    "user":{ // Added if no user context provided to identify who made such reaction
      "documentId": "njx99iv4p4txuqp307ye8625",
      "username": "mziarko+1@virtuslab.com",
      "email": "mziarko+1@virtuslab.com"
    }
  },
  // ...
]

List all reactions of kind / type associated with Content Type

GraphQL equivalent: Public GraphQL API -> List all reactions of kind / type associated with Content Type

GET <host>/api/reactions/list/<type slug>/single/<single type UID>?locale=<locale code> GET <host>/api/reactions/list/<type slug>/collection/<collection type UID>/<documentId>?locale=<locale code>

Return all reactions of specific kind / type assiciated with provided Collection / Single Type UID and Content Type Document ID with following combinations:

  • all - if you're not providing the user context via Authorization header
  • all related with user - if call is done with user context via Authorization header

Example URL: https://localhost:1337/api/reactions/list/like/single/api::homepage.homepage?locale=en Example URL: https://localhost:1337/api/reactions/list/like/collection/api::post.post/njx99iv4p4txuqp307ye8625?locale=en

Example response body

[
  {
    "documentId": "njx99iv4p4txuqp307ye8625",
    "createdAt": "2023-09-14T20:13:01.649Z",
    "updatedAt": "2023-09-14T20:13:01.670Z",
    "user":{ // Added if no user context provided to identify who made such reaction
      "documentId": "njx99iv4p4txuqp307ye8625",
      "username": "mziarko+1@virtuslab.com",
      "email": "mziarko+1@virtuslab.com"
    }
  },
  // ...
]

Set reaction for Content Type

GraphQL equivalent: Public GraphQL API -> Set reaction for Content Type

POST <host>/api/reactions/set/<type slug>/single/<single type UID>?locale=<locale code> POST <host>/api/reactions/set/<type slug>/collection/<collection type UID>/<documentId>?locale=<locale code>

Create reaction of specific kind / type assiciated with provided Collection / Single Type UID and Content Type Document ID.

Authorization header is required

Example URL: https://localhost:1337/api/reactions/set/like/single/api::homepage.homepage?locale=en Example URL: https://localhost:1337/api/reactions/set/like/collection/api::post.post/njx99iv4p4txuqp307ye8625?locale=en

Example response body

{
  "documentId": "njx99iv4p4txuqp307ye8625",
  "createdAt": "2023-09-14T20:13:01.649Z",
  "updatedAt": "2023-09-14T20:13:01.670Z",
  "relatedUid": "api::post.post:njx99iv4p4txuqp307ye8625"
}

Unset reaction for Content Type

GraphQL equivalent: Public GraphQL API -> Unset reaction for Content Type

DELETE <host>/api/reactions/unset/<type slug>/single/<single type UID>?locale=<locale code> DELETE <host>/api/reactions/unset/<type slug>/collection/<collection type UID>/<documentId>?locale=<locale code>

Delete reaction of specific kind / type assiciated with provided Collection / Single Type UID and Content Type Document ID.

Authorization header is required

Example URL: https://localhost:1337/api/reactions/unset/like/single/api::homepage.homepage?locale=en Example URL: https://localhost:1337/api/reactions/unset/like/collection/api::post.post/njx99iv4p4txuqp307ye8625?locale=en

Example response body

true

Toggle reaction for Content Type

GraphQL equivalent: Public GraphQL API -> Toggle reaction for Content Type

POST <host>/api/reactions/toggle/<type slug>/single/<single type UID>?locale=<locale code> POST <host>/api/reactions/toggle/<type slug>/collection/<collection type UID>/<documentId>?locale=<locale code>

Toggle reaction of specific kind / type assiciated with provided Collection / Single Type UID and Content Type Document ID.

Authorization header is required

Example URL: https://localhost:1337/api/reactions/toggle/like/single/api::homepage.homepage?locale=en Example URL: https://localhost:1337/api/reactions/toggle/like/collection/api::post.post/njx99iv4p4txuqp307ye8625?locale=en

Example response body

{
  "documentId": "njx99iv4p4txuqp307ye8625",
  "createdAt": "2023-09-14T20:13:01.649Z",
  "updatedAt": "2023-09-14T20:13:01.670Z",
  "relatedUid": "api::post.post:njx99iv4p4txuqp307ye8625"
}

// or

true

Possible scenarios

  1. No reaction set yet - after toggle reaction is set
  2. Single reaction already set - after toogle no reaction is set
  3. Multiple reactions already set - after toggle just specified reaction stays, rest becomes unset

🕸️ Public GraphQL API specification

Testing

To test all queries and understand the schemas use GraphQL Playground exposed by @strapi/plugin-graphql on http://localhost:1337/graphql

Get reaction kinds / types

REST API equivalent: Public REST API -> Get reaction kinds / types

Return a list of available reaction kinds to use on the end user interface and expose for interaction with users.

Example request

query {
  reactionKinds {
      slug
      name
      emoji
    icon {
      url
    }
  }
}

Example response body

{
  "data": {
    "reactionKinds": [
      {
        "slug": "like",
        "name": "Like",
        "emoji": "👍",
        "icon": null
      }
    ]
  }
}

List all reactions associated with Content Type

REST API equivalent: Public REST API -> List all reactions associated with Content Type

Return all reactions assiciated with provided Collection / Single Type UID and Content Type Document ID with following combinations:

  • Query reactionsListAll - no Authorization header provided (open for public)
  • Query reactionsListPerUser - an Authorization header is mandatory

Example request

query {
  reactionsListAll(uid: "api::post.post", documentId: "njx99iv4p4txuqp307ye8625", locale: "en") {
    documentId
    kind {
      slug
      name
      emoji
    }
    user {
      email
    }
  }
}
query {
  reactionsListPerUser(uid: "api::post.post", documentId: "njx99iv4p4txuqp307ye8625", locale: "en") {
    documentId
    kind {
      slug
      name
      emoji
    }
  }
}

Example response body

{
  "data": {
    "reactionsListAll": [
      {
        "documentId": "njx99iv4p4txuqp307ye8625",
        "kind": {
          "slug": "like",
          "name": "Like",
          "emoji": "👍"
        },
        "user": {
          "email": "mziarko+1@virtuslab.com"
        },
        "createdAt": "2023-09-14T20:13:01.670Z"
      }
    ]
  }
}
// --------------------------------
{
  "data": {
    "reactionsListPerUser": [
      {
        "documentId": "njx99iv4p4txuqp307ye8625",
        "kind": {
          "slug": "like",
          "name": "Like",
          "emoji": "👍"
        },
        "createdAt": "2023-09-14T20:13:01.670Z"
      }
    ]
  }
}

List all reactions of kind / type associated with Content Type

REST API equivalent: Public REST API -> List all reactions of kind / type associated with Content Type

Return all reactions of specific kind / type assiciated with provided Collection / Single Type UID and Content Type Document ID with following combinations:

  • Query reactionsListAll - no Authorization header provided (open for public)
  • Query reactionsListPerUser - an Authorization header is mandatory

Example request

query {
  reactionsListAll(kind: "like", uid: "api::post.post", documentId: "njx99iv4p4txuqp307ye8625", locale: "en") {
    documentId
    user {
      email
    }
    createdAt
  }
}
query {
  reactionsListPerUser(kind: "like", uid: "api::post.post", documentId: "njx99iv4p4txuqp307ye8625", locale: "en") {
    documentId
    createdAt
  }
}

Example response body

{
  "data": {
    "reactionsListAll": [
      {
        "documentId": "njx99iv4p4txuqp307ye8625",
        "user": {
          "email": "mziarko+1@virtuslab.com"
        },
        "createdAt": "2023-09-14T20:13:01.670Z"
      }
    ]
  }
}
// --------------------------------
{
  "data": {
    "reactionsListPerUser": [
      {
        "documentId": "njx99iv4p4txuqp307ye8625",
        "createdAt": "2023-09-14T20:13:01.670Z"
      }
    ]
  }
}

Set reaction for Content Type

REST API equivalent: Public REST API -> Set reaction for Content Type

Create reaction of specific kind / type assiciated with provided Collection / Single Type UID and Content Type Document ID.

Authorization header is required

Example request

mutation reactionSet {
  reactionSet(
    input: {
      kind: "like",
      uid: "api::post.post",
      documentId: "njx99iv4p4txuqp307ye8625",
      locale: "en"
    }
  ) {
    documentId
  }
}

Example response body

{
  "data": {
    "reactionSet": {
      "documentId": "njx99iv4p4txuqp307ye8625"
    }
  }
}

Unset reaction for Content Type

REST API equivalent: Public REST API -> Unset reaction for Content Type

Delete reaction of specific kind / type assiciated with provided Collection / Single Type UID and Content Type Document ID.

Authorization header is required

Example request

mutation reactionUnset {
  reactionUnset(
    input: {
      kind: "like",
      uid: "api::post.post",
      documentId: "njx99iv4p4txuqp307ye8625",
      locale: "en"
    }
  ) {
    documentId
  }
}

Example response body

{
  "data": {
    "reactionUnset": {
      "documentId": null
    }
  }
}

Toggle reaction for Content Type

REST API equivalent: Public REST API -> Toggle reaction for Content Type

Toggle reaction of specific kind / type assiciated with provided Collection / Single Type UID and Content Type Document ID.

Authorization header is required

Example request

mutation reactionToggle {
  reactionToggle(
    input: {
      kind: "like",
      uid: "api::post.post",
      documentId: "njx99iv4p4txuqp307ye8625",
      locale: "en"
    }
  ) {
    documentId
  }
}

Example response body

{
  "data": {
    "reactionToggle": {
      "documentId": "njx99iv4p4txuqp307ye8625"
    }
  }
}

// or

{
  "data": {
    "reactionToggle": {
      "documentId": null
    }
  }
}

Possible scenarios

  1. No reaction set yet - after toggle reaction is set
  2. Single reaction already set - after toogle no reaction is set
  3. Multiple reactions already set - after toggle just specified reaction stays, rest becomes unset

🔌 Enrich service for Strapi extensions

Enrich One

You can use this service method for example in your Strapi Content API findOne method to enrich the metadata of retrieved entity by associated reactions.

What is important, service method does not modify default data schema of Strapi Content API. All additions are made in the meta property in the form of:

// ...
"meta": {
  // ...
  "reactions": {
    "<type slug>": [
      // list of reactions
    ],
    // ...
  }
}

Example extension

// src/api/post/controllers/post.ts

'use strict';

/**
 *  post controller
 */

import { factories } from '@strapi/strapi'

export default factories.createCoreController('api::post.post', ({ strapi }) => ({
  async findOne(ctx) {
    const response = await super.findOne(ctx);

    return strapi
      .service('plugin::reactions.enrich')
      .enrichOne('api::post.post', response);
  },
}));

Example response

{
  "data":{
    "documentId": "njx99iv4p4txuqp307ye8625",
    "attributes":{
      // Content type attributes 
    }
  },
  "meta":{
    "reactions":{
      "like":[{
          "documentId": "njx99iv4p4txuqp307ye8625",
          "createdAt": "2023-09-14T20:13:01.649Z",
          "updatedAt": "2023-09-14T20:13:01.670Z",
          "relatedUid": "api::post.post:2",
          "kind":{
            "documentId": "njx99iv4p4txuqp307ye8625",
            "name": "Like",
            "slug": "like",
            "emoji": "👍",
            "emojiFallbackUrl": "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f44d.png",
            "icon": null
          },
          "user":{
            "documentId": "njx99iv4p4txuqp307ye8625",
            "username": "mziarko+1@virtuslab.com",
            "email": "mziarko+1@virtuslab.com"
          }
        },
        // ...
      ],
    // ...
    }
  }
}

Enrich Many

You can use this service method for example in your Strapi Content API find method to enrich the metadata of retrieved entities set by associated reactions.

What is important, service method does not modify default data schema of Strapi Content API. All additions are made in the meta property in the form of:

// ...
"meta": {
  // ...
  "reactions": {
    "<content type id>": {
      "<type slug>": [
        // list of reactions
      ],
      // ...
    }
  }
}

Example extension

// src/api/post/controllers/post.ts

'use strict';

/**
 *  post controller
 */

import { factories } from '@strapi/strapi'

export default factories.createCoreController('api::post.post', ({ strapi }) => ({
  async find(ctx) {
    const response = await super.find(ctx);

    return strapi
      .service('plugin::reactions.enrich')
      .enrichMany('api::post.post', response);
  },
}));

Example response

{
  "data": [{
      "documentId": "njx99iv4p4txuqp307ye8625",
      "attributes":{
        // Content type attributes 
      }
    }, {
      "documentId": 2,
      "attributes":{
        // Content type attributes 
      }
    }
  ],
  "meta":{
    "reactions":{
      "njx99iv4p4txuqp307ye8625": {
        "like":[{
            "documentId": "njx99iv4p4txuqp307ye8625",
            "createdAt": "2023-09-14T20:13:01.649Z",
            "updatedAt": "2023-09-14T20:13:01.670Z",
            "relatedUid": "api::post.post:2",
            "kind":{
              "documentId": "njx99iv4p4txuqp307ye8625",
              "name": "Like",
              "slug": "like",
              "emoji": "👍",
              "emojiFallbackUrl": "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f44d.png",
              "icon": null
            },
            "user":{
              "documentId": "njx99iv4p4txuqp307ye8625",
              "username": "mziarko+1@virtuslab.com",
              "email": "mziarko+1@virtuslab.com"
            }
          },
          // ...
        ],
      // ...
      },
      "2": {}
    }
  }
}

💬 FAQ

Import / Export

Q: I'm exporting / moving data between environments. Will I lost all reactions?

A: No you won't lost anything. Reactions plugin is based on real relations (Polymorphic relations) and all the data should be exported / moved and re-indexed properly on your new environment.

The only thing you might need to do after import is described below ⬇️


Q: I've imported data to the new instance and I'm not seeing reaction linked to my Content Types or they are wrong. What to do?

A: Most probably after the import unique identifiers of your Content Types has changed according to the new environment.

You might need to use one of special admin actions for the Reactions plugin like "Synchronize associations". To get access to it, you must be Super Admin or have assigned Settings: Admin permission to any of your roles.

This is not destructive action and it just goes through all existing Reactions and update their search key according to linked Content Type UID and ID like api::post.post:njx99iv4p4txuqp307ye8625.


🤝 Contributing

Feel free to fork and make a Pull Request to this plugin project. All the input is warmly welcome!

  1. Clone repository

    git clone git@github.com:VirtusLab-Open-Source/strapi-plugin-reactions.git
    
  2. Run install & watch:link command

    // Install all dependencies
    yarn install
    
    // Watch for file changes using `plugin-sdk` and follow the instructions provided by this official Strapi developer tool
    yarn watch:link
  3. Within the Strapi project, modify config/plugins.{js,ts} for imgix

//...
'reactions': {
  enabled: true,
  //...
}
//...
  1. Run your Strapi instance

👨‍💻 Community support

For general help using Strapi, please refer to the official Strapi documentation. For additional help, you can use one of these channels to ask a question:

  • Discord We're present on official Strapi Discord workspace. Find us by [VirtusLab] prefix and DM.
  • Slack - VirtusLab Open Source We're present on a public channel #strapi-molecules
  • GitHub (Bug reports, Contributions, Questions and Discussions)
  • E-mail - we will respond back as soon as possible

📝 License

MIT License Copyright (c) VirtusLab Sp. z o.o. & Strapi Solutions.

Readme

Keywords

Package Sidebar

Install

npm i strapi-plugin-reactions

Weekly Downloads

18

Version

2.0.1

License

MIT

Unpacked Size

484 kB

Total Files

100

Last publish

Collaborators

  • cyp3r