Background Cover
January 15, 2017
13 min read

Creando un plugin para Talk

jsjavascript react coral journal

Talk Plugin Pride

Talk expone una API amistosa para crear nuevas reacciones. Para explorar todas las capacidades de Talk vamos a crear una nueva reacción.

En Talk hay tres plugins para reaccionar ante los comentarios: Like, Love, Respect. Estas reacciones están separadas en plugins para que cada uno pueda agregar las reacciones que quiera en los comentarios.

Nuestro nuevo plugin podemos crearlo de cero o bien usar la CLI de Talk. En caso de que no conozcas el termino CLI éste hace referencia a Command Line Interface. Es decir, mediante línea de comandos exponemos utilidades para que sea más fácil la interacción e integración con Talk.

Creando un nuevo plugin usando Talk CLI

  • Abrimos la terminal
  • Vamos a la carpeta de Talk
  • Escribimos ./bin/cli-plugins

[image:8C12B7FA-8D04-4551-B0A7-27327675338A-11470-000050DB328C4C9F/Screen Shot 2018-03-05 at 10.23.40 AM.png]

La terminal nos va a mostrar 3 opciones. create, list y reconcile

  • create: Lo utilizamos para crear un nuevo plugin. Despliega un wizard y nos hace responder un par de preguntas para saber cómo está compuesto nuestro plugin.
  • list: Muestra una lista de todos nuestros plugins.
  • reconcile: Realiza la reconciliación de plugins locales y descarga sus dependencias externas.

Para crear nuestro plugin vamos a utilizar ./bin/cli-plugins create.

La CLI nos hará 4 preguntas:

[image:3C6EEED9-F487-44B2-BF3B-6F7912DAD583-11470-0000511AA4331517/Screen Shot 2018-03-05 at 10.28.01 AM.png]

Explicando las preguntas del cli-plugins create

  1. La primera es sobre el nombre de nuestro plugin.
  2. Este plugin extiende las capacidades del server? Si tu plugin necesita extender el schema de la base de datos, interactúa con rutas o servicios debes indicar que sí. En este caso nosotros necesitamos guardar que el usuario interactuó con un comentario mediante una reacción. Entonces indicamos que sí.
  3. Este plugin extiende las capacidades del cliente? Si tu plugin agrega contenido visual a Talk necesitamos marcar que si. En este caso necesitamos agregar un botón con el cual los usuarios puedan reaccionar ante los comentarios.
  4. Lo agregamos al plugins.json? Si indicamos que si nuestro plugin se va a activar instantáneamente. Como indicamos que si en el server y en el cliente este será embebido en ambos.

Luego de este proceso se creará un plugin dentro de la carpeta /plugins con el nombre que le asignamos. Nuestro plugin se llamapride-reaction

La estructura de nuestro plugin

La estructura de nuestro plugin luce así. Vayamos uno por uno para entender qué hace cada archivo y porqué es necesario.

[image:25FCA924-2222-4E72-BE84-7F91764E0689-11470-00005240EA06911D/Screen Shot 2018-03-05 at 10.49.29 AM.png]

  • index.js Dentro del archivo index tendremos todo lo que exportamos al server. En este caso solo veremos un module.exports = {}. Es decir, no extendemos aún el servidor. (Pero lo haremos más adelante)

  • /client Dentro de la carpeta client tendremos todos los archivos necesarios para extender el cliente.

    • index.js En este archivo indicaremos como vamos a extender nuestro cliente. Generalmente es útil para indicar donde el plugin será embebido. En nuestro caso queremos ponerlo en cada comentario. Más adelante veremos cómo hacer esto. También sirve para agregar funcionalidad como utilizar reducers y translations
    • .eslintrc.json Aquí se encuentran las reglas de ESLint. Por defecto están las que utiliza Talk.
    • translations.yml Este archivo no es obligatorio pero si querés soportar multiples lenguajes debes utilizarlo.
    • /components Acá se encuentran los componentes. Por defecto vamos a encontrar MyPluginComponent.js y sus estilos con CSS Modules MyPluginComponent.css

Al correr nuestra instancia de Talk veremos que el plugin se embebió perfectamente:

[image:8547EF1D-9852-49BF-9004-D067188322A7-11470-0000552C898FB2E2/Screen Shot 2018-03-05 at 11.43.09 AM.png]

Es importante destacar que Talk no controla la arquitectura de los plugins. Pero por razones de performance y consistencia es importante que sigamos ciertos lineamientos básicos.

Para crear componentes debes estar familiarizado con React. Si no lo estás, te recomiendo las guías oficiales. Components and Props - React

/Los archivos que vinieron por defecto con el creador de plugins podemos eliminarlos o reutilizarlos. Como prefieras!/

Ahora que sabemos que rol tiene cada archivo vamos a crear nuestro plugin :sunglasses:

Construyendo nuestro plugin

Lo primero que debemos pensar es en qué consiste nuestro plugin y qué experiencia queremos brindar. Sabemos que queremos que exista un botón, que pueda ser cliqueado y cree una reacción en el comentario. Vamos a crearlo.

Ya que nuestro botón es un componente creamos un nuevo archivo dentro de esa carpeta. Vamos a llamarlo PrideButton.js

La minima expresión de nuestro botón luce así:

import React from 'react';

class PrideButton extends React.Component {
  render() {
    return <button>Pride!</button>;
  }
}

export default PrideButton;

Bien. Creamos nuestro botón. Ahora queremos que se vea abajo de cada comentario. Para eso vamos a utilizar los slots. Los slots son pequeños lugares dentro de Talk donde podemos poner plugins. Talk ya tiene un lugar preparado para las reacciones. Este slot se llama commentReactions.

Para agregarlo a ese lugar vamos a ir al client/index.js y vamos a agregar las siguientes lineas.

import PrideButton from './components/PrideButton';

export default {
  slots: {
    commentReactions: [PrideButton]
  }
};

Notarás que borramos MyPluginComponent del objeto slots. Es porque ya no queremos mostrarlo. Podemos también borrar sus archivos si no vamos a utilizarlo. Aunque estos no serán agregados al bundle de Talk si no se encuentran exportados.

Si vamos a Talk veremos que nuestro PrideButton se embebió perfectamente en los comentaros pero todavía nos falta agregarle funcionalidad.

[image:4C8DE3A6-FD97-4EDA-8C2E-70C86A85A8DC-11470-0000560B38BC0F93/Screen Shot 2018-03-05 at 11.58.28 AM.png]

Agregando funcionalidad con la API de Talk

Talk expone una serie de herramientas para la creación de plugins. En este caso podemos utilizar withReaction. withReaction es un HOC (High Order Component) que le agrega funcionalidad a nuestros componentes.

Lo utilizamos así:

import React from 'react';
import { withReaction } from 'plugin-api/beta/client/hocs';

class PrideButton extends React.Component {
  render() {
    return <button>Pride!</button>;
  }
}

export default withReaction('pride')(PrideButton);

El primer parámetro que le pasamos a withReactions es el nombre de la reacción. En nuestro caso ‘pride’ o lo que elijamos. Debemos ser consistentes con esto ya que esto queda impactado en las bases de datos luego.

Vamos a crear la funcionalidad del click para generar una reacción o eliminarla en caso de que ya hayan reaccionado (con la misma reacción) en el comentario.

import React from 'react';
import { withReaction } from 'plugin-api/beta/client/hocs';

class PrideButton extends React.Component {
  handleClick = () => {
    const { postReaction, deleteReaction, alreadyReacted } = this.props;

    if (alreadyReacted) {
      deleteReaction();
    } else {
      postReaction();
    }
  };

  render() {
    return <button onClick={this.handleClick}>Pride!</button>;
  }
}

export default withReaction('pride')(PrideButton);

withReactions hace que el componente reciba postReaction, deleteReaction y alreadyReacted

  • postReaction : Postea la reacción al servido. <Función>
  • deleteReaction: Elimina la reacción <Función>
  • alreadyReacted: Nos indica si ya reaccionaron con esa reacción. <Booleano>
  • count: Número que nos indica la cantidad de veces que los usuarios reaccionaron ante el comentario. <Entero>

Ahora para que todo esto funcione nos falta agregar algo en nuestro index.js de la carpeta de nuestro plugin. Esta vez para extender el servidor.

const { getReactionConfig } = require('../../plugin-api/beta/server');
module.exports = getReactionConfig('pride');

getReactionConfig le agrega la funcionalidad necesaria del lado del servidor.

Ahora nuestro plugin funciona!

Pero visualmente se ve horrible y no sabemos si ya reaccionamos o no al comentario. Cambiemos eso.

Agregando CSS

Vamos a crear un PrideButton.css dentro de la carpeta components.

Algo bien simple para notar visualmente:

.reacted {
  background: red;
}

.button {
  background: wheat;
}
import React from 'react';
import styles from './PrideButton.css';
import { withReaction } from 'plugin-api/beta/client/hocs';

class PrideButton extends React.Component {
  handleClick = () => {
    const { postReaction, deleteReaction, alreadyReacted } = this.props;

    if (alreadyReacted) {
      deleteReaction();
    } else {
      postReaction();
    }
  };

  render() {
    const { alreadyReacted, count } = this.props;
    return (
      <button
        className={alreadyReacted ? styles.reacted : styles.button}
        onClick={this.handleClick}
      >
        Orgullo!
        {count > 0 && count}
      </button>
    );
  }
}

export default withReaction('pride')(PrideButton);

Con esto finaliza la creación básica de un plugin


Customizando la UI de los plugins

Talk API expone una serie de herramientas para la UI: coral-uI. Dentro de coral-ui hay iconos, botones, alertas y más. Podemos ver todos los componentes que existen dentro de la carpeta raíz de Talk client/coral-ui.

Usando Coral-UI

Para usar la UI de Coral usamos el siguiente código:

import { Icon, Button } from 'plugin-api/beta/client/components/ui';

const myButton = () => (
  <Button>
    <Icon name="favorite" />
    Favorito
  </Button>
);

/El componente Icon utiliza Material Icons. Aquí podrás ver toda la lista de iconos y sus respectivos nombres Material icons - Material Design /

En el futuro subiremos información detallada sobre los componentes de UI a la documentación.

Utilizando SVGs

Para los iconos del pride-plugin decidí utilizar Sketch, ya que ninguno de los iconos de Material me convencían.

Empecé por crear 2 estados:

  • El icono inactivo y sin reacción
  • El icono con reacción

Para darle más experiencia al usuario pensé que el icono inactivo podría ser en escala de grises y el que ya tiene reacción podría ser con color. Se me ocurrió que un arcoíris podría quedar bien.

[image:22437E16-8155-44F6-A3D2-28EC2C2EBB09-11470-000059CAF321B54A/Screen Shot 2018-03-05 at 1.07.38 PM.png]

Exportar / Copiar código SVG

Sketch tiene una forma de exportar el código SVG:

  • Botón derecho en el grupo
  • Cliquear “Copy SVG code” o “Copiar código SVG”

[image:EE95F6E6-1A0B-4E99-9EBC-A35CCB4ECDD7-1204-000012D5E674B745/Screen Shot 2018-03-07 at 12.02.21 PM.png]

Podemos exportarlo como archivo o copiar el código inline a nuestro componente. Prefiero tener el código inline para tener más control sobre las clases y la customización. En este caso, me gustaría pasarle distintas paletas de colores, una gris: greyscale y otra de colores: colored.

Entonces creamos RainBowIcon.js y escribimos el siguiente código

import React from 'react';
import PropTypes from 'prop-types';

// Las paletas de colores que vamos a utilizar
const colorPalette = {
  grayscale: ['#C6C6C6', '#C6C6C6', '#7E7E7E', '#7C7C7C', '#7C7C7C', '#9F9F9F'],
  colored: ['#F5C15F', '#EB7835', '#EB5242', '#CB4AB0', '#49B1DE', '#61C482']
};

const RainbowIcon = ({ paletteType = 'colored', palette = [] }) => {
  return (
    <svg
      width="19px"
      height="9px"
      viewBox="0 0 19 9"
      version="1.1"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
        <g transform="translate(-492.000000, -630.000000)">
          <g transform="translate(492.000000, 630.000000)">
            <path
              d="M9.5,0 C4.24785714,0 0,4.02428571 0,9 L2.71428571,9 C2.71428571,5.45142857 5.75428571,2.57142857 9.5,2.57142857 C13.2457143,2.57142857 16.2857143,5.45142857 16.2857143,9 L19,9 C19,4.02428571 14.7521429,0 9.5,0 Z"
              fill={palette[0] || colorPalette[paletteType][0]}
            />
            <path
              d="M9.5,1 C4.80071429,1 1,4.57714286 1,9 L3.42857143,9 C3.42857143,5.84571429 6.14857143,3.28571429 9.5,3.28571429 C12.8514286,3.28571429 15.5714286,5.84571429 15.5714286,9 L18,9 C18,4.57714286 14.1992857,1 9.5,1 Z"
              fill={palette[1] || colorPalette[paletteType][1]}
            />
            <path
              d="M9.5,2 C5.35357143,2 2,5.13 2,9 L4.14285714,9 C4.14285714,6.24 6.54285714,4 9.5,4 C12.4571429,4 14.8571429,6.24 14.8571429,9 L17,9 C17,5.13 13.6464286,2 9.5,2 Z"
              fill={palette[2] || colorPalette[paletteType][2]}
            />
            <path
              d="M9.5,3 C5.90642857,3 3,5.68285714 3,9 L4.85714286,9 C4.85714286,6.63428571 6.93714286,4.71428571 9.5,4.71428571 C12.0628571,4.71428571 14.1428571,6.63428571 14.1428571,9 L16,9 C16,5.68285714 13.0935714,3 9.5,3 Z"
              fill={palette[3] || colorPalette[paletteType][3]}
            />
            <path
              d="M9.5,4 C6.45928571,4 4,6.23571429 4,9 L5.57142857,9 C5.57142857,7.02857143 7.33142857,5.42857143 9.5,5.42857143 C11.6685714,5.42857143 13.4285714,7.02857143 13.4285714,9 L15,9 C15,6.23571429 12.5407143,4 9.5,4 Z"
              fill={palette[4] || colorPalette[paletteType][4]}
            />
            <path
              d="M9.5,5 C7.01214286,5 5,6.78857143 5,9 L6.28571429,9 C6.28571429,7.42285714 7.72571429,6.14285714 9.5,6.14285714 C11.2742857,6.14285714 12.7142857,7.42285714 12.7142857,9 L14,9 C14,6.78857143 11.9878571,5 9.5,5 Z"
              fill={palette[5] || colorPalette[paletteType][5]}
            />
          </g>
        </g>
      </g>
    </svg>
  );
};

// Esto es importante así nos aseguramos que pasamos propiedades correctas al componente
RainbowIcon.propTypes = {
  paletteType: PropTypes.oneOf(['colored', 'grayscale']),
  palette: PropTypes.array
};

export default RainbowIcon;

La mayoría del componente es código generado por Sketch. Salvo las propiedades fill que las controlo con las paletas. El color de las lineas del arcoíris se darán en base al orden del los colores de la paleta.

Las props del componente:paletteType y palette

  • paletteType : Ya que creamos 2 paletas en el componente acá podemos pasar el nombre directamente: colored greyscale

  • palette : Si queremos pasarle un array de colores lo hacemos usando esta propiedad

Listo. Ya tenemos nuestro icono. Ahora modifiquemos el botón PrideButton.js para poder usarlo.

import React from 'react';
import cn from 'classnames';
import styles from './PrideButton.css';
import { withReaction } from 'plugin-api/beta/client/hocs';
import RainbowIcon from './RainbowIcon';

class PrideButton extends React.Component {
  handleClick = () => {
    const { postReaction, deleteReaction, alreadyReacted } = this.props;

    if (alreadyReacted) {
      deleteReaction();
    } else {
      postReaction();
    }
  };

  render() {
    const { alreadyReacted } = this.props;
    return (
      <div className={cn(styles.container, 'talk-plugin-pride-container')}>
        <a className={cn(styles.button, 'talk-plugin-pride-button')} onClick={this.handleClick}>
          {alreadyReacted ? <RainbowIcon /> : <RainbowIcon paletteType="grayscale" />}
        </a>
      </div>
    );
  }
}

export default withReaction('pride')(PrideButton);

Utilizamos la propiedad alreadyReacted para cambiar el icono y renderizar uno en escala de grises (utilizando la propiedad grayscale

Hay muchos pros y cons de usar SVGs inline. Para saber más sobre esto: 5 Gotchas You’re Gonna Face Getting Inline SVG Into Production | CSS-Tricks El código fuente hasta este punto es first commit · coralproject/talk-plugin-pride@ae5c1a5 · GitHub Por temas de performance y dado que esta porción de código SVG no puede ser cacheada vamos a crear archivos SVG separados para los dos estados del icono.

Creamos la carpeta assets y dos archivos dentro de ella: ColoredRainbowIcon.svg y GrayscaleRainbowIcon.svg. Ambos los podemos exportar con Sketch o simplemente copiar el código SVG en cada uno.

Utilizar SVG en los componentes

Vamos a importarlos igual que como hacemos con los componentes, solo que agregamos su extensión .svg al final.

import ColoredRainbowIcon from '../assets/ColoredRainbowIcon.svg';
import GrayscaleRainbowIcon from '../assets/GrayscaleRainbowIcon.svg’;

Ya que Webpack nos dará la nueva url del recurso, su uso es de la siguiente manera:

<img src="{ColoredRainbowIcon}" className="{cn(styles.icon," `${plugin}-icon`)} />

Utilizando media queries

Seguramente quieras soportar varios dispositivos y necesitamos que nuestro plugin responda correctamente. Para esto podemos utilizar media queries.

En este caso, queremos que en dispositivos móviles o menores a 425px no se muestre la etiqueta de la reacción.

@media (max-width: 425px) {
  .label {
    display: none;
  }
}

Si miras nuestra configuración de PostCSS notarás que utilizamos precss. PreCSS nos deja utilizar una sintaxis similar a la de Sass opcionalmente.

Es decir que podemos hacer uso de variables

@custom-media --viewport-medium (width <= 50rem);
@custom-selector :--heading h1, h2, h3, h4, h5, h6;

:root {
  --fontSize: 1rem;
  --mainColor: #12345678;
}

@media (--viewport-medium) {
  body {
    color: var(--mainColor);
    font-family: system-ui;
    font-size: var(--fontSize);
    line-height: calc(var(--fontSize) * 1.5);
    overflow-wrap: break-word;
    padding-inline: calc((var(--fontSize) / 2) + 1px);
  }
}

Para saber más sobre PreCSS: https://github.com/jonathantneal/precss

Agregando animaciones

Para hacer aún más divertida la experiencia del usuario, quería que cuando el usuario cliquee despliegue una pequeña animación.

.reacted {
  animation: rainbow 1s 1;
}

@keyframes rainbow {
  20% {
    color: #eb5242;
  }
  40% {
    color: #f5c15f;
  }
  60% {
    color: #61c482;
  }
  80% {
    color: #49b1de;
  }
  100% {
    color: #eb7835;
  }
}

 Ahora agregamos condicionalmente estos estilos utilizando la librería para clases classnames

<button
  className={cn(
    styles.button,
    {[styles.reacted]: alreadyReacted}
  )}
  onClick={this.handleClick}
>

Listo! Ahora cada vez que alguien cliquee se activa el estilo reacted y se gatilla la animación.

Finalizando nuestro plugin este quedaría así:

import React from 'react';
import cn from 'classnames';
import styles from './PrideButton.css';
import { withReaction } from 'plugin-api/beta/client/hocs';
import ColoredRainbowIcon from '../assets/ColoredRainbowIcon.svg';
import GrayscaleRainbowIcon from '../assets/GrayscaleRainbowIcon.svg';

const plugin = 'talk-plugin-pride';

class PrideButton extends React.Component {
  handleClick = () => {
    const { postReaction, deleteReaction, alreadyReacted, user } = this.props;

    // If the current user does not exist, trigger sign in dialog.
    if (!user) {
      showSignInDialog();
      return;
    }

    if (alreadyReacted) {
      deleteReaction();
    } else {
      postReaction();
    }
  };

  render() {
    const { count, alreadyReacted } = this.props;
    return (
      <div className={cn(styles.container, `${plugin}-container`)}>
        <button
          className={cn(
            styles.button,
            {
              [`${styles.reacted} talk-plugin-pride-reacted`]: alreadyReacted
            },
            `${plugin}-button`
          )}
          onClick={this.handleClick}
        >
          <span className={cn(`${plugin}-label`, styles.label)}>Pride</span>
          {alreadyReacted ? (
            <img src={ColoredRainbowIcon} className={cn(styles.icon, `${plugin}-icon`)} />
          ) : (
            <img src={GrayscaleRainbowIcon} className={cn(styles.icon, `${plugin}-icon`)} />
          )}
          <span className={cn(`${plugin}-count`)}>{count > 0 && count}</span>
        </button>
      </div>
    );
  }
}

export default withReaction('pride')(PrideButton);

Para ver el completo código fuente https://github.com/coralproject/talk-plugin-pride