← Back to blog

Moderating comments through the Perspective API Firebase Extension

Darren Ackers

Lead Developer

19th January, 2023

Need a custom tool? Invertase can help

Tired of investing in off-the-shelf software that doesn't quite fit your business? Invertase can build solutions tailored to your exact needs. Tell us what you're looking for here and we'll be in touch.

This article looks to show how we can moderate comments in real-time using Firebase Extensions.

Overview

In the past two decades, we have witnessed an explosion of social media content, through blogs, chat rooms…an almost limitless number of resources that allow users online to add their thoughts.

Not all comments and thoughts are received well, however. Through threatening to volatile wording, we can be on the receiving end of unwelcome messages.

In the past, websites and apps have used manual moderation, paid works, or volunteers who read through the content, and typically with guidelines choose which content can be allowed to view by the rest of the public!

You can see the final application below.

Automating Moderation

To make this easier, Jigsaw recently published their Firebase Extension which utilizes the Perspective API. By installing the extension, we will demonstrate here how to build this in the app to speed up and improve moderation.

Installing the Firebase Extension

Before we start coding, and to help better understand the purpose of this tutorial. We will first need to add the Extension from the Marketplace.

Navigate to the link and click to `Install in Console`.

Follow the instructions, making sure that the default values remain the same.

Make sure to look up your Perspective API Key before updating, read the following guide to obtain your unique key.

Finally, select Install Extension to add to your project.

Setting up the project

In this example, we are going to take advantage of Zapp by Invertase. This allows us to quickly prototype Flutter applications in the browser and to share and fork as we develop. Feel free to fork this example afterward to play around with this feature.

Getting started

Navigate to create a new project and select A new Flutter project

Navigate to pubspec.yaml and set the following as your dependencies:

dependencies:
  flutter:
    sdk: flutter

  firebase_auth: ^4.1.5
  firebase_core: ^2.3.0
  google_sign_in: 5.4.2
  cloud_firestore: ^4.1.0


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2

Make sure to click “Pub Get” to download the latest dependencies for your project.

Installing Firebase

  1. Create a new Firebase project, if you don’t have one already.
  2. Navigate to ⚙️ => Project Settings.
  3. Select to add a new App.
  4. Choose the Flutter icon.

Now, we have our configuration, let’s create a new file in our project lib folder called firebase_options.dart. Make sure to fill in your own personal configuration at the start of the file

// Copyright 2022, the Chromium project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// File generated by FlutterFire CLI.
// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
    show defaultTargetPlatform, kIsWeb, TargetPlatform;

const apiKey = "AIzaSyBCYB_dLL98LFbZwjtHL9cku88TIcmXyrI";
const authDomain = "fir-extensions-workshop.firebaseapp.com";
const projectId = "fir-extensions-workshop";
const storageBucket = "fir-extensions-workshop.appspot.com";
const messagingSenderId = "1024408426888";
const appId = "1:1024408426888:web:f7f5ef85495696502856c7";
const databaseURL = 'https://fir-extensions-workshop-default-rtdb.firebaseio.com';
const iosBundleId = 'com.example.firebaseExtensionsWorkshop';
const iosClientId = '1024408426888-65v1b9ffv6sgfqngfjab5ol5qdikh2rm.apps.googleusercontent.com';
const macOS_androidClientId = '1024408426888-l14jokf2sbafqvf94bo2vpkds5h4rpon.apps.googleusercontent.com';
const ios_androidClientId = '1024408426888-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com';

class DefaultFirebaseOptions {
  static FirebaseOptions get currentPlatform {
    if (kIsWeb) {
      return web;
    }

    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
        return android;
      case TargetPlatform.iOS:
        return ios;
      case TargetPlatform.macOS:
        return macos;
      case TargetPlatform.windows:
        throw UnsupportedError(
          'DefaultFirebaseOptions have not been configured for windows - '
          'you can reconfigure this by running the FlutterFire CLI again.',
        );
      case TargetPlatform.linux:
        throw UnsupportedError(
          'DefaultFirebaseOptions have not been configured for linux - '
          'you can reconfigure this by running the FlutterFire CLI again.',
        );
      default:
        throw UnsupportedError(
          'DefaultFirebaseOptions are not supported for this platform.',
        );
    }
  }

  static const FirebaseOptions web = FirebaseOptions(
      apiKey: apiKey,
      authDomain: authDomain,
      projectId: projectId,
      storageBucket: storageBucket,
      messagingSenderId: messagingSenderId,
      appId: appId);

  static const FirebaseOptions android = FirebaseOptions(
    apiKey: apiKey,
    appId: '1:1024408426888:android:899c6485cfce26c13574d0',
    messagingSenderId: messagingSenderId,
    projectId: projectId,
    databaseURL: databaseURL,
    storageBucket: storageBucket,
  );

  static const FirebaseOptions ios = FirebaseOptions(
    apiKey: apiKey,
    appId: '1:1024408426888:android:b5ed8a545949abdd2dc23d',
    messagingSenderId: messagingSenderId,
    projectId: projectId,
    databaseURL: databaseURL,
    storageBucket: storageBucket,
    androidClientId: ios_androidClientId,
    iosClientId: iosClientId,
    iosBundleId: iosBundleId,
  );

  static const FirebaseOptions macos = FirebaseOptions(
    apiKey: apiKey,
    appId: '1:1024408426888:android:b5ed8a545949abdd2dc23d',
    messagingSenderId: messagingSenderId,
    projectId: projectId,
    databaseURL: databaseURL,
    storageBucket: storageBucket,
    androidClientId: macOS_androidClientId,
    iosClientId: iosClientId,
    iosBundleId: iosBundleId,
  );
}

Next, we will add anonymous authentication through Firebase. This will provide a uid for the current users to that we can link a comment.

First, we are required to enable anonymous authentication. Navigate to the authentication section of your Firebase console -> Authentication -> Sign-In method -> Add new provider.

Select anonymous -> enable -> Save

To read and write Firestore document data, we will need to allow permissions. Navigate to your Firestore database and add the following Firestore rule:

match /comments/{commentId} {
   allow read, write;
}

Building the Home Page

Ok, let’s start coding the app.

Anonymous Authentication

When the application first loads, we will need to ensure that we have a user to post comments with, let’s start by adding a widget wrapper to handle authentication.

Create a folder called components in the lib folder and add a file called authentication.dart.

Next, paste the following:

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';

class Authentication extends StatelessWidget {
  final Widget children;
  const Authentication({super.key, required this.children});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
        stream: FirebaseAuth.instance.authStateChanges(),
        initialData: FirebaseAuth.instance.currentUser,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return CircularProgressIndicator();
          }

          if (snapshot.hasData) {
            return children;
          } else {
            return CircularProgressIndicator();
          }
        });
  }
}

This segment, allows a single child widget to be presented as the child in the widget.

The streamBuilder, connects through Firebase Authentication, and returns the current user.

When a user is found, the widget will return the provided child widget!

Setting the main dart file

Now we have the widgets required to run an authenticated app, navigate to the main.dart file and add the following:

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'firebase_options.dart';

import 'components/authentication.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      darkTheme: ThemeData.dark(),
      theme: ThemeData.light(),
      home: const MyHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    /* Build the initial scoffold for the page. */
    return Scaffold(
        appBar: AppBar(
          title: Text("Comments Sample"),
        ),
        /** 
         * Add the authentication wrapper
         */
        body: Authentication(
            children: Padding(
                padding: EdgeInsets.fromLTRB(20, 20, 20, 20),
                child: Column(
                    children: [Text("Comments Sample")]))));
  }
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  /* Automatically sign in annonomously */
  await FirebaseAuth.instance.signInAnonymously();

  /* Persist authentication */
  await FirebaseAuth.instance.setPersistence(Persistence.LOCAL);

  /* Start the application */
  runApp(MyApp());
}

Run the app and you should now see the application working; after displaying a loading icon the page will then show the text “Comments Sample”

OK, so now have an app running and we are authenticated!

Let’s next create a widget for displaying our comments, this will:

  • Show no comments available as the default behavior
  • Display a full list of comments when available, showing the name of the user (defaulting to anonymous) and displaying the message that the users have provided.

Add a file called comments.dart in our components folder:

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class Comments extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final Stream<QuerySnapshot> _commentsStream =
                FirebaseFirestore.instance.collection('comments').orderBy("createdAt", descending: false).snapshots();

    return StreamBuilder<QuerySnapshot>(
        stream: _commentsStream,
        builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
          if (snapshot.hasError) {
            return Text(snapshot.error.toString());
          }

          if (snapshot.connectionState == ConnectionState.waiting) {
            return Text("Loading");
          }

          if (snapshot.data!.docs.isEmpty) return Text("No Comments Available");

          return new Scaffold(
            body: SizedBox(
              height: 1000,
              child: ListView(
                shrinkWrap: true,
                children: snapshot.data!.docs.map((DocumentSnapshot document) {
                  Map<String, dynamic> data =
                      document.data()! as Map<String, dynamic>;

                  double? attribute_scores =
                      data["attribute_scores"]?["TOXICITY"] ?? null;

                  if (attribute_scores == null)
                    return ListTile(
                      title: Text(data['name'] ?? "Annonymous"),
                      subtitle: Text("Moderating comment..."),
                    );

                  if (attribute_scores! > 0.5)
                    return ListTile(
                      title: Text("Toxic message found, cannot display..."),
                      subtitle: Text(data['name'] ?? "Annonymous"),
                    );
                  ;

                  print('attribute: $attribute_scores');

                  return ListTile(
                    title: Text(data['text']),
                    subtitle: Text(data['name'] ?? "Annonymous"),
                  );
                }).toList(),
              ),
            ),
          );
        });
  }
}

There is a lot to take in here, so let’s go through what this widget is achieving.

Firstly, we set up a listener on our `comments` collection, this matches the value that was defaulted when we Installed the Firebase Extension.

If no comments are included in the collection, the widget will display “No comments available”

When comments are available, the widget will map through each item:

  • If the `attribute_scores` toxicity rating is not available, then the text provided by the user and the message “Moderating comment will display”.
  • If a rating has been provided but it has been rated as too toxic then the comment will display with “Toxic message found, cannot display…”. In this example, we sat the hard-coded value of 0.5 as the score at which we deem the comment to be toxic.
  • Otherwise, the comment has passed the check and the comment is successfully displayed with the name of the user (anonymous) along with the provided message.

We can now add this widget to our main.dart file:

import 'components/comments.dart';

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    /* Build the initial scaffold for the page. */
    return Scaffold(
        appBar: AppBar(
          title: Text("Comments Sample"),
        ),
        /** 
         * Add the authentication wrapper
         * Add comments
         */
        body: Authentication(
            children: Padding(
                padding: EdgeInsets.fromLTRB(20, 20, 20, 20),
                child: Column(
                    children: [Expanded(child: Comments())]))));
  }
}

Re-running the app, you should now see:

As the final step, we now need to add a widget to add comments.

But first, some housekeeping on our future comments collection, before we add more messages it is important to note that Firestore does not automatically present documents in their natural order. To ensure comments appear in `ascending` order.

Navigate to your Firestore console and select the Indexes tab.

Add a new index for createdAt
:

This will ensure that when including a timestamp value for the document createdAt field, all comments appear in the order that they were added.

Create another file in the components folder called add_comment.dart.

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class AddComment extends StatelessWidget {
  AddComment();

  @override
  Widget build(BuildContext context) {
    final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
    TextEditingController commentEditingController = TextEditingController();

    // Create a CollectionReference called users that references the firestore collection
    CollectionReference comments =
        FirebaseFirestore.instance.collection("comments");

    Future<void> addComment(user) async {
      print(_formKey.currentState);
      // Call the user's CollectionReference to add a new user
      await comments
          .add({
            // 'name': user!.displayName,
            // 'photoUrl': user!.photoURL,
            "text": commentEditingController.text,
            "createdAt": DateTime.now().millisecondsSinceEpoch
          })
          .then((value) => print("Comment Added"))
          .catchError((error) => print("Failed to add comment: $error"));

      _formKey.currentState?.reset();
    }

    return StreamBuilder(
      stream: FirebaseAuth.instance.authStateChanges(),
      initialData: FirebaseAuth.instance.currentUser,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const CircularProgressIndicator();
        }

        if (snapshot.hasData) {
          return Form(
            key: _formKey,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                TextFormField(
                  controller: commentEditingController,
                  decoration: const InputDecoration(
                    hintText: 'Enter a comment',
                  ),
                  validator: (String? value) {
                    if (value == null || value.isEmpty) {
                      return 'Please enter some text';
                    }
                    return null;
                  },
                ),
                Padding(
                  padding: const EdgeInsets.symmetric(vertical: 16.0),
                  child: ElevatedButton(
                    onPressed: () {
                      // Validate will return true if the form is valid, or false if
                      // the form is invalid.
                      if (_formKey.currentState!.validate()) {
                        addComment(snapshot.data);
                      }
                    },
                    child: const Text('Submit'),
                  ),
                ),
              ],
            ),
          );
        } else {
          return Container(child: Text("Please sign in to add comments"));
        }
      },
    );
  }
}

In this example, we have included a streamBuilder to ensure that we have a valid user for adding a comment. This user information can then be used in the future development to use for customizing comments in the future, for example:

  • Display Name
  • Profile picture

For the purposes of this demo, we will have anonymous authentication so those fields will not be available. Instead of the user’s name, we will simply use the Id.

This simple form, allows the user to input a message. Selecting submit will add a new document for our new comment.

Let’s add this to our main.dart file:

import 'components/add_comment.dart';

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    /* Build the initial scoffold for the page. */
    return Scaffold(
        appBar: AppBar(
          title: Text("Comments Sample"),
        ),
        /** 
         * Add the authentication wrapper
         * Add comments and add comments
         */
        body: Authentication(
            children: Padding(
                padding: EdgeInsets.fromLTRB(20, 20, 20, 20),
                child: Column(
                    children: [Expanded(child: Comments()), AddComment()]))));
  }
}

The UI should now show this new widget at the bottom of the screen:

Tip: When adding a comment, monitoring the Firestore console will provide a visual insight into how the extension updates the document data.

Go ahead and a new comment.

The comments section will update automatically

After moderating, valid comments will appear in the list.

Toxic comments, however, will no longer be shown!

You can see from the rating, how toxic the comment was rated.

Summary

In this tutorial, we have learned how to create a Flutter app that allows for the moderation of text in your database. There are so many other ways to test the validation of your content and ways to handle that in your UI.

Feel free to clone the project in Zapp or elsewhere and share your ideas with us. We’d love to see what developers can create using the power of Firebase Extensions!

Stay tuned for more updates and exciting news that we will share in the future. Follow us on Invertase TwitterLinkedin, and Youtube, and subscribe to our monthly newsletter to stay up-to-date. You may also join our Discord to have an instant conversation with us.

Darren Ackers

Lead Developer

Categories

Tags