<commit_before>/** post.js
* @file: /src/app/routes/post.js
* @description: Handles express routing for the POST routes
* @parameters: Object(app), Object(resources)
* @exports: Express routes
*/

var busboy = require('connect-busboy')
var body_parser = require('body-parser')
var urlencodedParser = body_parser.urlencoded({ extended: false })
var fs = require('fs')
var md5 = require('md5')
var getSlug = require('speakingurl')
var ObjectID = require('mongodb').ObjectID

module.exports = function (app, resources) {
  var functions = resources.functions
  var image_processer = resources.image_processer

  app.post('/submit', urlencodedParser, function (req, res, next) {
    // Controller for handling page inputs.
    // ## TODO ######################################################
    // # Add error handling, check and sanitize inputs              #
    // ##############################################################

    var id = req.body.id
    var type = req.body.type
    var hidden = (req.body.hidden) ? true : false;
    var isodate = functions.getISOdate()
    var niceurl = getSlug(req.body.title, {
      // URL Settings
      separator: '-',
      maintainCase: false,
      symbols: false
    })

    var usersdb = resources.collections.users
    var pagesdb = resources.collections.pages
    var revisionsdb = resources.collections.revisions

    usersdb.find({ _id: new ObjectID(req.user._id), blocked: false }).toArray(function(err, result) {
      if(result.length > 0) {
        //Proceed as normal
        if(req.body.cover_image_id == 'undefined' || req.body.cover_image_filename == 'undefined') {
          cover_image_id = null
          cover_image_filename = null
        } else {
          cover_image_id = req.body.cover_image_id
          cover_image_filename = req.body.cover_image_filename
        }

        if(type == 1) { // Fakta
          var data = {
            title: req.body.title,
            url: niceurl,
            type: type,
            post: {
              content: req.body.content,
              sources: {
                  name: typeof(req.body.source_name) === 'string' ? [req.body.source_name] : req.body.source_name,
                  url: typeof(req.body.source_url) === 'string' ? [req.body.source_url] : req.body.source_url,
              },
              type: req.body.type,
              license: req.body.license,
              license_cc_version: req.body.license_cc_version,
              license_holder: req.body.license_holder,
              license_holder_website: req.body.license_holder_website,
              cover: {
                id: cover_image_id,
                filename: cover_image_filename
              }
            }, rating: {
              likes: 0
            }, user_info: {
              id: req.user._id,
              hidden: hidden
            }
          }
        } else if(type == 2) { // Recept
          var data = {
            title: req.body.title,
            url: niceurl,
            type: type,
            post: {
              content: req.body.content,
              sources: {
                  name: typeof(req.body.source_name) === 'string' ? [req.body.source_name] : req.body.source_name,
                  url: typeof(req.body.source_url) === 'string' ? [req.body.source_url] : req.body.source_url,
              },
              food_type: req.body.food_type,
              license: req.body.license,
              license_cc_version: req.body.license_cc_version,
              license_holder: req.body.license_holder,
              license_holder_website: req.body.license_holder_website,
              cover: {
                id: cover_image_id,
                filename: cover_image_filename
              }
            }, rating: {
              likes: 0,
              votes: 0,
              votes_sum: 0
            }, user_info: {
              id: req.user._id,
              hidden: hidden
            }
          }
        } else if(type == 3) { // Restaurang
          var data = {
            title: req.body.title,
            url: niceurl,
            type: type,
            post: {
              content: req.body.content,
              sources: {
                  name: typeof(req.body.source_name) === 'string' ? [req.body.source_name] : req.body.source_name,
                  url: typeof(req.body.source_url) === 'string' ? [req.body.source_url] : req.body.source_url,
              },
              city: req.body.city,
              street: req.body.street,
              coordinates: {
                  latitude: req.body.latitude,
                  longitude: req.body.longitude
              },
              phone: req.body.phone,
              website: req.body.website,
              email: req.body.email,
              license: req.body.license,
              license_cc_version: req.body.license_cc_version,
              license_holder: req.body.license_holder,
              license_holder_website: req.body.license_holder_website,
              veg_offer: req.body.veg_offer,
              food: req.body.food,
              hashtag: req.body.hashtag,
              openhours: {
                monday: req.body.monday,
                tuesday: req.body.tuesday,
                wednesday: req.body.wednesday,
                thursday: req.body.thursday,
                friday: req.body.friday,
                saturday: req.body.saturday,
                sunday: req.body.sunday
              }, cover: {
                id: cover_image_id,
                filename: cover_image_filename
              },
            }, rating: {
                likes: 0,
                votes: 0,
                votes_sum: 0
            }, user_info: {
              id: req.user._id,
              hidden: hidden
            }
          }
        } else if(type == 4) { // Produkt
          var data = {
            title: req.body.title,
            url: niceurl,
            type: type,
            post: {
              content: req.body.content,
              sources: {
                  name: typeof(req.body.source_name) === 'string' ? [req.body.source_name] : req.body.source_name,
                  url: typeof(req.body.source_url) === 'string' ? [req.body.source_url] : req.body.source_url,
              },
              license: req.body.license,
              license_cc_version: req.body.license_cc_version,
              license_holder: req.body.license_holder,
              license_holder_website: req.body.license_holder_website,
              veg_type: req.body.veg_type,
              product_type: req.body.product_type,
              manufacturer: req.body.manufacturer,
              manufacturer_website: req.body.manufacturer_website,
              hashtag: req.body.hashtag,
              cover: {
                id: cover_image_id,
                filename: cover_image_filename
              }
            }, rating: {
              likes: 0,
              votes: 0,
              votes_sum: 0
            }, user_info: {
              id: req.user._id,
              hidden: hidden
            }
          }
        } else if(type == 5) { // Butik
          var data = {
            title: req.body.title,
            url: niceurl,
            type: type,
            post: {
              content: req.body.content,
              sources: {
                  name: typeof(req.body.source_name) === 'string' ? [req.body.source_name] : req.body.source_name,
                  url: typeof(req.body.source_url) === 'string' ? [req.body.source_url] : req.body.source_url,
              },
              license: req.body.license,
              license_cc_version: req.body.license_cc_version,
              license_holder: req.body.license_holder,
              license_holder_website: req.body.license_holder_website,
              city: req.body.city,
              street: req.body.street,
              coordinates: {
                  latitude: req.body.latitude,
                  longitude: req.body.longitude
              },
              website: req.body.website,
              hashtag: req.body.hashtag,
              openhours: {
                monday: req.body.monday,
                tuesday: req.body.tuesday,
                wednesday: req.body.wednesday,
                thursday: req.body.thursday,
                friday: req.body.friday,
                saturday: req.body.saturday,
                sunday: req.body.sunday
              },
              cover: {
                id: cover_image_id,
                filename: cover_image_filename
              }
            }, rating: {
              likes: 0,
              votes: 0,
              votes_sum: 0
            }, user_info: {
              id: req.user._id,
              hidden: hidden
            }
          }
        } else {
          res.redirect('/ny')
        }

        data.accepted = null

        if(id) {
          id = new ObjectID(id) //If editing the post, the id will be provided as a string and we need to convert it to an objectid
          if('contributors' in data.user_info) {
            //Check if this user has already contributed to the post, otherwise add the user to the array
            var alreadyContributed = false

            for(var userObj in data.user_info.contributors) {
              var userObjId = data.user_info.contributors[userObj].id
              if(req.user._id == userObjId) {
                alreadyContributed = true
              }
            }

            if(!alreadyContributed) {
              data.user_info.contributors.push({
                id: req.user._id,
                hidden: hidden
              })
            }
          }
        } else {
          //This is the first revision, add the user creating it to the list of contributors
          data.user_info.contributors = [{
            id: req.user._id,
            hidden: hidden
          }]
        }

        pagesdb.count({ _id : id }, function(err, count) {
          if(err) throw err
          if(count > 0) {
            //Update
            revisionsdb.find({ post_id : id }).toArray(function(err, result) {
              if(result.length > 0) {
                var revision = result[0]
                revision.modified = isodate //Update date modified
                revision.pending += 1 //Update revisions pending moderation for this post
                revision_number = (new Date(isodate).getTime() / 1000) //This is a unix timestamp
                revision.revisions[revision_number] = data.post

                revision.revisions[revision_number].meta = {
                  accepted: null,
                  user_info: {
                    id: req.user._id,
                    hidden: hidden
                  },
                  timestamp: {
                    created: isodate,
                    updatedby: req.user._id
                  }
                }

                revisionsdb.update({ post_id : id}, revision, function(err, result) {
                  res.redirect('/ny/uppdaterad/?newpost='+niceurl)
                })
              } else {
                console.log('No entry for this page in revisions collection')
                pagesdb.find({ _id : id }).toArray(function(err, result) {
                  data.timestamp = {
                    created: result[0].timestamp.created,
                    update: isodate, // Add timestamp for update
                    updatedby: req.user._id
                  }

                  pagesdb.update({_id:id}, data, function(err, result) {
                    res.redirect('/ny/uppdaterad/?newpost='+niceurl)
                  })
                })
              }
            })
          } else {
            //Insert
            data.timestamp = {}
            data.timestamp.created = isodate // Add timestamp for creation
            pagesdb.insert(data, function(err, doc) {
              if(err) throw err

              var revision_number = (new Date(isodate).getTime() / 1000) //This is a unix timestamp
              var revision = {
                post_id: doc.ops[0]._id,
                pending: 1, //Represents total number of revisions that are awaiting moderation, at insertion, just this one
                modified: isodate,
                revision: revision_number,
                revisions: {},
              }

              revision.revisions[revision_number] = data.post
              revision.revisions[revision_number].meta = {
                accepted: null,
                user_info: data.user_info,
                timestamp: {
                  created: isodate
                }
              }

              revisionsdb.insert(revision, function(err, doc) {
                //Should probably make a redirect to a page showing that the page is updated and will be published after moderation
                res.redirect('/ny/publicerad/?newpost='+niceurl)
              })
            })
          }
        })
      } else {
        //User is blocked, should not be able to send this request in the first place
        next()
      }
    })
  })

  app.post('/submit/file', function(req, res) {
    var fstream
    req.pipe(req.busboy)
    req.busboy.on('file', function (fieldname, file, filename) {
      var imagesdb = resources.collections.images
      imagesdb.count({ }, function (err, num_rows) {
        if (err) throw err
        random = Math.floor((Math.random() * 99999999) + 10000000);
        uHash = md5(num_rows + 1 + random)
        randomsubstring = Math.floor((Math.random() * 20) + 10)
        uFilename = uHash.substring(0, randomsubstring)

        imagesdb.insert({ id:num_rows + 1, filename: uFilename, active: false, deleted: false, "user_info":{ id: req.user._id } }, function(err, doc) {
          if(err) throw err

          var filePath = resources.config.uploads
          var fileNameOriginal = filePath + '/' + uFilename + '_original.jpg'
          fstream = fs.createWriteStream(fileNameOriginal)
          file.pipe(fstream)
          fstream.on('finish', function() {
            var resize = image_processer.resize(uFilename)
            if(resize == true) {
              fstream.on('close', function () {
                res.send(doc.ops[0]._id)
              })
            }
          })
        })
      })
    })
  })

  app.post('/submit/file/avatar', function(req, res) {
  var fstream
  req.pipe(req.busboy)
  req.busboy.on('file', function (fieldname, file, filname) {
    fstream = fs.createWriteStream(resources.config.uploads + '/avatar/' + req.user._id + '_raw.jpg')
    file.pipe(fstream)
    fstream.on('finish', function() {
      var usersdb = resources.collections.users
      var resize = image_processer.avatar(req.user._id)
      if(resize == true) {
        fstream.on('close', function () {
          usersdb.update({
              _id : new ObjectID(req.user._id)
            }, {
            $set: {
              "active_photo": 'vegosvar', // This can be fb or vegosvar, later gr. For easy switching later on
              "vegosvar_photo": '/avatar/' + req.user._id + '.jpg'
            }
            }, function(err, status) {
              if(err) throw err
              res.send(req.user._id)
            })
          })
        }
      })
    })
  })

}<commit_msg>Fix url slug by replacing characters before calling getSlug, also change 'blocked' to 'info.blocked'<commit_after>/** post.js
* @file: /src/app/routes/post.js
* @description: Handles express routing for the POST routes
* @parameters: Object(app), Object(resources)
* @exports: Express routes
*/

var busboy = require('connect-busboy')
var body_parser = require('body-parser')
var urlencodedParser = body_parser.urlencoded({ extended: false })
var fs = require('fs')
var md5 = require('md5')
var getSlug = require('speakingurl')
var ObjectID = require('mongodb').ObjectID

module.exports = function (app, resources) {
  var functions = resources.functions
  var image_processer = resources.image_processer

  app.post('/submit', urlencodedParser, function (req, res, next) {
    var id = req.body.id
    var type = req.body.type
    var hidden = (req.body.hidden) ? true : false;
    var isodate = functions.getISOdate()

    //Prevent speakingurl from converting swedish characters to ae and oe by replacing them with what we want
    var tamperedTitle = String(req.body.title).toLowerCase().replace(/å|ä/g, 'a').replace(/ö/g, 'o')
    var niceurl = getSlug(tamperedTitle, {
      // URL Settings
      separator: '-',
      maintainCase: false,
      symbols: false
    })
    var simpleSlug = getSlug(tamperedTitle, {separator: ''})

    var usersdb = resources.collections.users
    var pagesdb = resources.collections.pages
    var revisionsdb = resources.collections.revisions

    usersdb.find({ _id: new ObjectID(req.user._id), "info.blocked": false }).toArray(function(err, result) {
      if(result.length > 0) {
        //Proceed as normal
        if(req.body.cover_image_id == 'undefined' || req.body.cover_image_filename == 'undefined') {
          cover_image_id = null
          cover_image_filename = null
        } else {
          cover_image_id = req.body.cover_image_id
          cover_image_filename = req.body.cover_image_filename
        }

        if(type == 1) { // Fakta
          var data = {
            title: req.body.title,
            url: niceurl,
            type: type,
            post: {
              content: req.body.content,
              sources: {
                  name: typeof(req.body.source_name) === 'string' ? [req.body.source_name] : req.body.source_name,
                  url: typeof(req.body.source_url) === 'string' ? [req.body.source_url] : req.body.source_url,
              },
              type: req.body.type,
              license: req.body.license,
              license_cc_version: req.body.license_cc_version,
              license_holder: req.body.license_holder,
              license_holder_website: req.body.license_holder_website,
              cover: {
                id: cover_image_id,
                filename: cover_image_filename
              }
            }, rating: {
              likes: 0
            }, user_info: {
              id: req.user._id,
              hidden: hidden
            }
          }
        } else if(type == 2) { // Recept
          var data = {
            title: req.body.title,
            url: niceurl,
            type: type,
            post: {
              content: req.body.content,
              sources: {
                  name: typeof(req.body.source_name) === 'string' ? [req.body.source_name] : req.body.source_name,
                  url: typeof(req.body.source_url) === 'string' ? [req.body.source_url] : req.body.source_url,
              },
              food_type: req.body.food_type,
              license: req.body.license,
              license_cc_version: req.body.license_cc_version,
              license_holder: req.body.license_holder,
              license_holder_website: req.body.license_holder_website,
              cover: {
                id: cover_image_id,
                filename: cover_image_filename
              }
            }, rating: {
              likes: 0,
              votes: 0,
              votes_sum: 0
            }, user_info: {
              id: req.user._id,
              hidden: hidden
            }
          }
        } else if(type == 3) { // Restaurang
          var data = {
            title: req.body.title,
            url: niceurl,
            type: type,
            post: {
              content: req.body.content,
              sources: {
                  name: typeof(req.body.source_name) === 'string' ? [req.body.source_name] : req.body.source_name,
                  url: typeof(req.body.source_url) === 'string' ? [req.body.source_url] : req.body.source_url,
              },
              city: req.body.city,
              street: req.body.street,
              coordinates: {
                  latitude: req.body.latitude,
                  longitude: req.body.longitude
              },
              phone: req.body.phone,
              website: req.body.website,
              email: req.body.email,
              license: req.body.license,
              license_cc_version: req.body.license_cc_version,
              license_holder: req.body.license_holder,
              license_holder_website: req.body.license_holder_website,
              veg_offer: req.body.veg_offer,
              food: req.body.food,
              hashtag: req.body.hashtag,
              openhours: {
                monday: req.body.monday,
                tuesday: req.body.tuesday,
                wednesday: req.body.wednesday,
                thursday: req.body.thursday,
                friday: req.body.friday,
                saturday: req.body.saturday,
                sunday: req.body.sunday
              }, cover: {
                id: cover_image_id,
                filename: cover_image_filename
              },
            }, rating: {
                likes: 0,
                votes: 0,
                votes_sum: 0
            }, user_info: {
              id: req.user._id,
              hidden: hidden
            }
          }
        } else if(type == 4) { // Produkt
          var data = {
            title: req.body.title,
            url: niceurl,
            type: type,
            post: {
              content: req.body.content,
              sources: {
                  name: typeof(req.body.source_name) === 'string' ? [req.body.source_name] : req.body.source_name,
                  url: typeof(req.body.source_url) === 'string' ? [req.body.source_url] : req.body.source_url,
              },
              license: req.body.license,
              license_cc_version: req.body.license_cc_version,
              license_holder: req.body.license_holder,
              license_holder_website: req.body.license_holder_website,
              veg_type: req.body.veg_type,
              product_type: req.body.product_type,
              manufacturer: req.body.manufacturer,
              manufacturer_website: req.body.manufacturer_website,
              hashtag: req.body.hashtag,
              cover: {
                id: cover_image_id,
                filename: cover_image_filename
              }
            }, rating: {
              likes: 0,
              votes: 0,
              votes_sum: 0
            }, user_info: {
              id: req.user._id,
              hidden: hidden
            }
          }
        } else if(type == 5) { // Butik
          var data = {
            title: req.body.title,
            url: niceurl,
            type: type,
            post: {
              content: req.body.content,
              sources: {
                  name: typeof(req.body.source_name) === 'string' ? [req.body.source_name] : req.body.source_name,
                  url: typeof(req.body.source_url) === 'string' ? [req.body.source_url] : req.body.source_url,
              },
              license: req.body.license,
              license_cc_version: req.body.license_cc_version,
              license_holder: req.body.license_holder,
              license_holder_website: req.body.license_holder_website,
              city: req.body.city,
              street: req.body.street,
              coordinates: {
                  latitude: req.body.latitude,
                  longitude: req.body.longitude
              },
              website: req.body.website,
              hashtag: req.body.hashtag,
              openhours: {
                monday: req.body.monday,
                tuesday: req.body.tuesday,
                wednesday: req.body.wednesday,
                thursday: req.body.thursday,
                friday: req.body.friday,
                saturday: req.body.saturday,
                sunday: req.body.sunday
              },
              cover: {
                id: cover_image_id,
                filename: cover_image_filename
              }
            }, rating: {
              likes: 0,
              votes: 0,
              votes_sum: 0
            }, user_info: {
              id: req.user._id,
              hidden: hidden
            }
          }
        } else {
          res.redirect('/ny')
        }

        data.accepted = null
        data.slug = simpleSlug

        if(id) {
          id = new ObjectID(id) //If editing the post, the id will be provided as a string and we need to convert it to an objectid
          if('contributors' in data.user_info) {
            //Check if this user has already contributed to the post, otherwise add the user to the array
            var alreadyContributed = false

            for(var userObj in data.user_info.contributors) {
              var userObjId = data.user_info.contributors[userObj].id
              if(req.user._id == userObjId) {
                alreadyContributed = true
              }
            }

            if(!alreadyContributed) {
              data.user_info.contributors.push({
                id: req.user._id,
                hidden: hidden
              })
            }
          }
        } else {
          //This is the first revision, add the user creating it to the list of contributors
          data.user_info.contributors = [{
            id: req.user._id,
            hidden: hidden
          }]
        }

        pagesdb.count({ _id : id }, function(err, count) {
          if(err) throw err
          if(count > 0) {
            //Update
            revisionsdb.find({ post_id : id }).toArray(function(err, result) {
              if(result.length > 0) {
                var revision = result[0]
                revision.modified = isodate //Update date modified
                revision.pending += 1 //Update revisions pending moderation for this post
                revision_number = (new Date(isodate).getTime() / 1000) //This is a unix timestamp
                revision.revisions[revision_number] = data.post

                revision.revisions[revision_number].meta = {
                  accepted: null,
                  user_info: {
                    id: req.user._id,
                    hidden: hidden
                  },
                  timestamp: {
                    created: isodate,
                    updatedby: req.user._id
                  }
                }

                revisionsdb.update({ post_id : id}, revision, function(err, result) {
                  res.redirect('/ny/uppdaterad/?newpost='+niceurl)
                })
              } else {
                console.log('No entry for this page in revisions collection')
                pagesdb.find({ _id : id }).toArray(function(err, result) {
                  data.timestamp = {
                    created: result[0].timestamp.created,
                    update: isodate, // Add timestamp for update
                    updatedby: req.user._id
                  }

                  pagesdb.update({_id:id}, data, function(err, result) {
                    res.redirect('/ny/uppdaterad/?newpost='+niceurl)
                  })
                })
              }
            })
          } else {
            //Insert
            data.timestamp = {}
            data.timestamp.created = isodate // Add timestamp for creation
            pagesdb.insert(data, function(err, doc) {
              if(err) throw err

              var revision_number = (new Date(isodate).getTime() / 1000) //This is a unix timestamp
              var revision = {
                post_id: doc.ops[0]._id,
                pending: 1, //Represents total number of revisions that are awaiting moderation, at insertion, just this one
                modified: isodate,
                revision: revision_number,
                revisions: {},
              }

              revision.revisions[revision_number] = data.post
              revision.revisions[revision_number].meta = {
                accepted: null,
                user_info: data.user_info,
                timestamp: {
                  created: isodate
                }
              }

              revisionsdb.insert(revision, function(err, doc) {
                //Should probably make a redirect to a page showing that the page is updated and will be published after moderation
                res.redirect('/ny/publicerad/?newpost='+niceurl)
              })
            })
          }
        })
      } else {
        //User is blocked, should not be able to send this request in the first place
        next()
      }
    })
  })

  app.post('/submit/file', function(req, res) {
    var fstream
    req.pipe(req.busboy)
    req.busboy.on('file', function (fieldname, file, filename) {
      var imagesdb = resources.collections.images
      imagesdb.count({ }, function (err, num_rows) {
        if (err) throw err
        random = Math.floor((Math.random() * 99999999) + 10000000);
        uHash = md5(num_rows + 1 + random)
        randomsubstring = Math.floor((Math.random() * 20) + 10)
        uFilename = uHash.substring(0, randomsubstring)

        imagesdb.insert({ id:num_rows + 1, filename: uFilename, active: false, deleted: false, "user_info":{ id: req.user._id } }, function(err, doc) {
          if(err) throw err

          var filePath = resources.config.uploads
          var fileNameOriginal = filePath + '/' + uFilename + '_original.jpg'
          fstream = fs.createWriteStream(fileNameOriginal)
          file.pipe(fstream)
          fstream.on('finish', function() {
            var resize = image_processer.resize(uFilename)
            if(resize == true) {
              fstream.on('close', function () {
                res.send(doc.ops[0]._id)
              })
            }
          })
        })
      })
    })
  })

  app.post('/submit/file/avatar', function(req, res) {
  var fstream
  req.pipe(req.busboy)
  req.busboy.on('file', function (fieldname, file, filname) {
    fstream = fs.createWriteStream(resources.config.uploads + '/avatar/' + req.user._id + '_raw.jpg')
    file.pipe(fstream)
    fstream.on('finish', function() {
      var usersdb = resources.collections.users
      var resize = image_processer.avatar(req.user._id)
      if(resize == true) {
        fstream.on('close', function () {
          usersdb.update({
              _id : new ObjectID(req.user._id)
            }, {
            $set: {
              "active_photo": 'vegosvar', // This can be fb or vegosvar, later gr. For easy switching later on
              "vegosvar_photo": '/avatar/' + req.user._id + '.jpg'
            }
            }, function(err, status) {
              if(err) throw err
              res.send(req.user._id)
            })
          })
        }
      })
    })
  })

}<|endoftext|>


<commit_before>/**
 * @fileOverview Loader module for restartless addons
 * @author       SHIMODA "Piro" Hiroshi
 * @version      4
 *
 * @license
 *   The MIT License, Copyright (c) 2010-2011 SHIMODA "Piro" Hiroshi.
 *   https://github.com/piroor/restartless/blob/master/license.txt
 * @url http://github.com/piroor/restartless
 */

/** You can customize shared properties for loaded scripts. */
var _namespacePrototype = {
		Cc : Components.classes,
		Ci : Components.interfaces,
		Cu : Components.utils,
		Cr : Components.results,
		Application : Components.classes['@mozilla.org/fuel/application;1']
						.getService(Components.interfaces.fuelIApplication)
	};
var _namespaces;

/**
 * This functiom loads specified script into a unique namespace for the URL.
 * Namespaces for loaded scripts have a wrapped version of this function.
 * Both this and wrapped work like as Components.utils.import().
 * Due to the reserved symbol "import", we have to use another name "load"
 * instead it.
 *
 * @param {String} aScriptURL
 *   URL of a script. Wrapped version of load() can handle related path.
 *   Related path will be resolved based on the location of the caller script.
 * @param {Object=} aExportTargetForImport
 *   EXPORTED_SYMBOLS in the loaded script will be exported to the object.
 *   If no object is specified, symbols will be exported to the global object
 *   of the caller.
 * @param {Object=} aExportTargetForRequire
 *   Properties of "exports" in the loaded script will be exported to the object.
 *
 * @returns {Object}
 *   The global object for the loaded script.
 */
function load(aURISpec, aExportTargetForImport, aExportTargetForRequire, aRoot)
{
	if (!_namespaces)
		_namespaces = {};
	var ns;
	if (aURISpec in _namespaces) {
		ns = _namespaces[aURISpec];
		_exportForImport(ns, aExportTargetForImport);
		_exportForRequire(ns, aExportTargetForRequire);
		return ns;
	}
	ns = _createNamespace(aURISpec, aRoot || aURISpec);
	try {
		Components.classes['@mozilla.org/moz/jssubscript-loader;1']
			.getService(Components.interfaces.mozIJSSubScriptLoader)
			.loadSubScript(aURISpec, ns);
	}
	catch(e) {
		dump('Loader::load('+aURISpec+') failed!\n'+e+'\n');
		throw e;
	}
	_exportForImport(ns, aExportTargetForImport);
	_exportForRequire(ns, aExportTargetForRequire);
	return _namespaces[aURISpec] = ns;
}

// JavaScript code module style
function _exportForImport(aSource, aTarget)
{
	if (
		!aTarget ||
		!('EXPORTED_SYMBOLS' in aSource) ||
		!aSource.EXPORTED_SYMBOLS ||
		!aSource.EXPORTED_SYMBOLS.forEach
		)
		return;
	for each (var symbol in aSource.EXPORTED_SYMBOLS)
	{
		aTarget[symbol] = aSource[symbol];
	}
}

// CommonJS style
function _exportForRequire(aSource, aTarget)
{
	if (
		!aTarget ||
		!('exports' in aSource) ||
		!aSource.exports ||
		typeof aSource.exports != 'object'
		)
		return;
	for (var symbol in aSource.exports)
	{
		aTarget[symbol] = aSource.exports[symbol];
	}
}

var IOService = Components.classes['@mozilla.org/network/io-service;1']
					.getService(Components.interfaces.nsIIOService);
var FileHandler = IOService.getProtocolHandler('file')
					.QueryInterface(Components.interfaces.nsIFileProtocolHandler);

/**
 * Checks existence of the file specified by the given relative path and the base URI.
 *
 * @param {String} aPath
 *   A relative path to a file or directory, from the aBaseURI.
 * @param {String} aBaseURI
 *   An absolute URI (with scheme) for relative paths.
 *
 * @returns {String}
 *   If the file (or directory) exists, returns the absolute URI. Otherwise null.
 */
function exists(aPath, aBaseURI)
{
	if (/^\w+:/.test(aPath)) {
		let leafName = aPath.match(/([^\/]+)$/);
		leafName = leafName ? leafName[1] : '' ;
		aBaseURI = aPath.replace(/(?:[^\/]+)$/, '');
		aPath = leafName;
	}
	var baseURI = aBaseURI.indexOf('file:') == 0 ?
					IOService.newFileURI(FileHandler.getFileFromURLSpec(aBaseURI)) :
					IOService.newURI(aBaseURI, null, null);
	if (aBaseURI.indexOf('jar:') == 0) {
		baseURI = baseURI.QueryInterface(Components.interfaces.nsIJARURI);
		var reader = Components.classes['@mozilla.org/libjar/zip-reader;1']
						.createInstance(Components.interfaces.nsIZipReader);
		reader.open(baseURI.JARFile.QueryInterface(Components.interfaces.nsIFileURL).file);
	    try {
			let baseEntry = baseURI.JAREntry.replace(/[^\/]+$/, '');
			let entries = reader.findEntries(baseEntry + aPath + '$');
			let found = entries.hasMore();
			return found ? baseURI.resolve(aPath) : null ;
		}
		finally {
			reader.close();
		}
	}
	else {
		let resolved = baseURI.resolve(aPath);
		return FileHandler.getFileFromURLSpec(resolved).exists() ? resolved : null ;
	}
}

function doAndWait(aAsyncTask)
{
	const Cc = Components.classes;
	const Ci = Components.interfaces;

	var done = false;
	var returnedValue = void(0);
	var continuation = function(aReturnedValue) {
			done = true;
			returnedValue = aReturnedValue;
		};

	var timer = Cc['@mozilla.org/timer;1']
					.createInstance(Ci.nsITimer);
	timer.init(function() {
		aAsyncTask(continuation);
	}, 0, Ci.nsITimer.TYPE_ONE_SHOT);

	var thread = Cc['@mozilla.org/thread-manager;1']
					.getService(Ci.nsIThreadManager)
					.currentThread;
	while (!done) {
		thread.processNextEvent(true);
	}
	return returnedValue;
}

function _createNamespace(aURISpec, aRoot)
{
	var baseURI = aURISpec.indexOf('file:') == 0 ?
					IOService.newFileURI(FileHandler.getFileFromURLSpec(aURISpec)) :
					IOService.newURI(aURISpec, null, null);
	var rootURI = typeof aRoot == 'string' ?
					(aRoot.indexOf('file:') == 0 ?
						IOService.newFileURI(FileHandler.getFileFromURLSpec(aRoot)) :
						IOService.newURI(aRoot, null, null)
					) :
					aRoot ;
	var ns = {
			__proto__ : _namespacePrototype,
			location : _createFakeLocation(baseURI),
			exists : function(aPath, aBase) {
				return exists(aPath, aBase || baseURI.spec);
			},
			exist : function(aPath, aBase) { // alias
				return exists(aPath, aBase || baseURI.spec);
			},
			/** JavaScript code module style */
			load : function(aURISpec, aExportTarget) {
				if (!/\.jsm?$/.test(aURISpec)) {
					if (exists(aURISpec+'.js', baseURI.spec))
						aURISpec += '.js'
					else if (exists(aURISpec+'.jsm', baseURI.spec))
						aURISpec += '.jsm'
				}
				var resolved = baseURI.resolve(aURISpec);
				if (resolved == aURISpec)
					throw new Error('Recursive load!');
				return load(resolved, aExportTarget || ns, aExportTarget, rootURI);
			},
			'import' : function() { // alias
				return this.load.apply(this, arguments);
			},
			/**
			 * CommonJS style
			 * @url http://www.commonjs.org/specs/
			 */
			require : function(aURISpec) {
				if (!/\.jsm?$/.test(aURISpec)) {
					if (exists(aURISpec+'.js', baseURI.spec))
						aURISpec += '.js'
					else if (exists(aURISpec+'.jsm', baseURI.spec))
						aURISpec += '.jsm'
				}
				var resolved = (aURISpec.charAt(0) == '.' ? rootURI : baseURI ).resolve(aURISpec);
				if (resolved == aURISpec)
					throw new Error('Recursive load!');
				var exported = {};
				load(resolved, exported, exported, rootURI);
				return exported;
			},
			/* utility to resolve relative path from the file */
			resolve : function(aURISpec, aBaseURI) {
				var base = !aBaseURI ?
								baseURI :
							aBaseURI.indexOf('file:') == 0 ?
								IOService.newFileURI(FileHandler.getFileFromURLSpec(aURISpec)) :
								IOService.newURI(aURISpec, null, null) ;
				return base.resolve(aURISpec);
			},
			doAndWait : function(aAsyncTask) {
				return doAndWait(aAsyncTask);
			},
			exports : {}
		};
	return ns;
}

function _createFakeLocation(aURI)
{
	aURI = aURI.QueryInterface(Components.interfaces.nsIURL)
					.QueryInterface(Components.interfaces.nsIURI);
	return {
		href     : aURI.spec,
		search   : aURI.query ? '?'+aURI.query : '' ,
		hash     : aURI.ref ? '#'+aURI.ref : '' ,
		host     : aURI.scheme == 'jar' ? '' : aURI.hostPort,
		hostname : aURI.scheme == 'jar' ? '' : aURI.host,
		port     : aURI.scheme == 'jar' ? -1 : aURI.port,
		pathname : aURI.path,
		protocol : aURI.scheme+':',
		reload   : function() {},
		replace  : function() {},
		toString : function() {
			return this.href;
		}
	};
}

function _callHandler(aHandler, aReason)
{
	for (var i in _namespaces)
	{
		try {
			if (_namespaces[i][aHandler] &&
				typeof _namespaces[i][aHandler] == 'function')
				_namespaces[i][aHandler](aReason);
		}
		catch(e) {
			dump(i+'('+aHandler+', '+aReason+')\n'+e+'\n');
		}
	}
}

function registerResource(aName, aRoot)
{
	IOService.getProtocolHandler('resource')
		.QueryInterface(Components.interfaces.nsIResProtocolHandler)
		.setSubstitution(aName, aRoot);
}

function unregisterResource(aName)
{
	IOService.getProtocolHandler('resource')
		.QueryInterface(Components.interfaces.nsIResProtocolHandler)
		.setSubstitution(aName, null);
}

/** Handler for "install" of the bootstrap.js */
function install(aReason)
{
	_callHandler('iustall', aReason);
}

/** Handler for "uninstall" of the bootstrap.js */
function uninstall(aReason)
{
	_callHandler('uninstall', aReason);
}

/** Handler for "shutdown" of the bootstrap.js */
function shutdown(aReason)
{
	_callHandler('shutdown', aReason);

	for each (let ns in _namespaces)
	{
		for (let i in ns.exports)
		{
			if (ns.exports.hasOwnProperty(i))
				delete ns.exports[i];
		}
	}
	_namespaces = void(0);

	IOService = void(0);
	FileHandler = void(0);

	load = void(0);
	_exportSymbols = void(0);
	exists = void(0);
	_createNamespace = void(0);
	_callHandler = void(0);
	registerResource = void(0);
	unregisterResource = void(0);
	install = void(0);
	uninstall = void(0);
	shutdown = void(0);
}
<commit_msg>can work with Thunderbird and Fennec (there are no FUEL)<commit_after>/**
 * @fileOverview Loader module for restartless addons
 * @author       SHIMODA "Piro" Hiroshi
 * @version      5
 *
 * @license
 *   The MIT License, Copyright (c) 2010-2011 SHIMODA "Piro" Hiroshi.
 *   https://github.com/piroor/restartless/blob/master/license.txt
 * @url http://github.com/piroor/restartless
 */

/** You can customize shared properties for loaded scripts. */
var _namespacePrototype = {
		Cc : Components.classes,
		Ci : Components.interfaces,
		Cu : Components.utils,
		Cr : Components.results,
		Application : (
			'@mozilla.org/fuel/application;1' in Components.classes ?
				Components.classes['@mozilla.org/fuel/application;1']
					.getService(Components.interfaces.fuelIApplication) :
			'@mozilla.org/steel/application;1' in Components.classes ?
				Components.classes['@mozilla.org/steel/application;1']
					.getService(Components.interfaces.steelIApplication) :
			null
		)
	};
var _namespaces;

/**
 * This functiom loads specified script into a unique namespace for the URL.
 * Namespaces for loaded scripts have a wrapped version of this function.
 * Both this and wrapped work like as Components.utils.import().
 * Due to the reserved symbol "import", we have to use another name "load"
 * instead it.
 *
 * @param {String} aScriptURL
 *   URL of a script. Wrapped version of load() can handle related path.
 *   Related path will be resolved based on the location of the caller script.
 * @param {Object=} aExportTargetForImport
 *   EXPORTED_SYMBOLS in the loaded script will be exported to the object.
 *   If no object is specified, symbols will be exported to the global object
 *   of the caller.
 * @param {Object=} aExportTargetForRequire
 *   Properties of "exports" in the loaded script will be exported to the object.
 *
 * @returns {Object}
 *   The global object for the loaded script.
 */
function load(aURISpec, aExportTargetForImport, aExportTargetForRequire, aRoot)
{
	if (!_namespaces)
		_namespaces = {};
	var ns;
	if (aURISpec in _namespaces) {
		ns = _namespaces[aURISpec];
		_exportForImport(ns, aExportTargetForImport);
		_exportForRequire(ns, aExportTargetForRequire);
		return ns;
	}
	ns = _createNamespace(aURISpec, aRoot || aURISpec);
	try {
		Components.classes['@mozilla.org/moz/jssubscript-loader;1']
			.getService(Components.interfaces.mozIJSSubScriptLoader)
			.loadSubScript(aURISpec, ns);
	}
	catch(e) {
		dump('Loader::load('+aURISpec+') failed!\n'+e+'\n');
		throw e;
	}
	_exportForImport(ns, aExportTargetForImport);
	_exportForRequire(ns, aExportTargetForRequire);
	return _namespaces[aURISpec] = ns;
}

// JavaScript code module style
function _exportForImport(aSource, aTarget)
{
	if (
		!aTarget ||
		!('EXPORTED_SYMBOLS' in aSource) ||
		!aSource.EXPORTED_SYMBOLS ||
		!aSource.EXPORTED_SYMBOLS.forEach
		)
		return;
	for each (var symbol in aSource.EXPORTED_SYMBOLS)
	{
		aTarget[symbol] = aSource[symbol];
	}
}

// CommonJS style
function _exportForRequire(aSource, aTarget)
{
	if (
		!aTarget ||
		!('exports' in aSource) ||
		!aSource.exports ||
		typeof aSource.exports != 'object'
		)
		return;
	for (var symbol in aSource.exports)
	{
		aTarget[symbol] = aSource.exports[symbol];
	}
}

var IOService = Components.classes['@mozilla.org/network/io-service;1']
					.getService(Components.interfaces.nsIIOService);
var FileHandler = IOService.getProtocolHandler('file')
					.QueryInterface(Components.interfaces.nsIFileProtocolHandler);

/**
 * Checks existence of the file specified by the given relative path and the base URI.
 *
 * @param {String} aPath
 *   A relative path to a file or directory, from the aBaseURI.
 * @param {String} aBaseURI
 *   An absolute URI (with scheme) for relative paths.
 *
 * @returns {String}
 *   If the file (or directory) exists, returns the absolute URI. Otherwise null.
 */
function exists(aPath, aBaseURI)
{
	if (/^\w+:/.test(aPath)) {
		let leafName = aPath.match(/([^\/]+)$/);
		leafName = leafName ? leafName[1] : '' ;
		aBaseURI = aPath.replace(/(?:[^\/]+)$/, '');
		aPath = leafName;
	}
	var baseURI = aBaseURI.indexOf('file:') == 0 ?
					IOService.newFileURI(FileHandler.getFileFromURLSpec(aBaseURI)) :
					IOService.newURI(aBaseURI, null, null);
	if (aBaseURI.indexOf('jar:') == 0) {
		baseURI = baseURI.QueryInterface(Components.interfaces.nsIJARURI);
		var reader = Components.classes['@mozilla.org/libjar/zip-reader;1']
						.createInstance(Components.interfaces.nsIZipReader);
		reader.open(baseURI.JARFile.QueryInterface(Components.interfaces.nsIFileURL).file);
	    try {
			let baseEntry = baseURI.JAREntry.replace(/[^\/]+$/, '');
			let entries = reader.findEntries(baseEntry + aPath + '$');
			let found = entries.hasMore();
			return found ? baseURI.resolve(aPath) : null ;
		}
		finally {
			reader.close();
		}
	}
	else {
		let resolved = baseURI.resolve(aPath);
		return FileHandler.getFileFromURLSpec(resolved).exists() ? resolved : null ;
	}
}

function doAndWait(aAsyncTask)
{
	const Cc = Components.classes;
	const Ci = Components.interfaces;

	var done = false;
	var returnedValue = void(0);
	var continuation = function(aReturnedValue) {
			done = true;
			returnedValue = aReturnedValue;
		};

	var timer = Cc['@mozilla.org/timer;1']
					.createInstance(Ci.nsITimer);
	timer.init(function() {
		aAsyncTask(continuation);
	}, 0, Ci.nsITimer.TYPE_ONE_SHOT);

	var thread = Cc['@mozilla.org/thread-manager;1']
					.getService(Ci.nsIThreadManager)
					.currentThread;
	while (!done) {
		thread.processNextEvent(true);
	}
	return returnedValue;
}

function _createNamespace(aURISpec, aRoot)
{
	var baseURI = aURISpec.indexOf('file:') == 0 ?
					IOService.newFileURI(FileHandler.getFileFromURLSpec(aURISpec)) :
					IOService.newURI(aURISpec, null, null);
	var rootURI = typeof aRoot == 'string' ?
					(aRoot.indexOf('file:') == 0 ?
						IOService.newFileURI(FileHandler.getFileFromURLSpec(aRoot)) :
						IOService.newURI(aRoot, null, null)
					) :
					aRoot ;
	var ns = {
			__proto__ : _namespacePrototype,
			location : _createFakeLocation(baseURI),
			exists : function(aPath, aBase) {
				return exists(aPath, aBase || baseURI.spec);
			},
			exist : function(aPath, aBase) { // alias
				return exists(aPath, aBase || baseURI.spec);
			},
			/** JavaScript code module style */
			load : function(aURISpec, aExportTarget) {
				if (!/\.jsm?$/.test(aURISpec)) {
					if (exists(aURISpec+'.js', baseURI.spec))
						aURISpec += '.js'
					else if (exists(aURISpec+'.jsm', baseURI.spec))
						aURISpec += '.jsm'
				}
				var resolved = baseURI.resolve(aURISpec);
				if (resolved == aURISpec)
					throw new Error('Recursive load!');
				return load(resolved, aExportTarget || ns, aExportTarget, rootURI);
			},
			'import' : function() { // alias
				return this.load.apply(this, arguments);
			},
			/**
			 * CommonJS style
			 * @url http://www.commonjs.org/specs/
			 */
			require : function(aURISpec) {
				if (!/\.jsm?$/.test(aURISpec)) {
					if (exists(aURISpec+'.js', baseURI.spec))
						aURISpec += '.js'
					else if (exists(aURISpec+'.jsm', baseURI.spec))
						aURISpec += '.jsm'
				}
				var resolved = (aURISpec.charAt(0) == '.' ? rootURI : baseURI ).resolve(aURISpec);
				if (resolved == aURISpec)
					throw new Error('Recursive load!');
				var exported = {};
				load(resolved, exported, exported, rootURI);
				return exported;
			},
			/* utility to resolve relative path from the file */
			resolve : function(aURISpec, aBaseURI) {
				var base = !aBaseURI ?
								baseURI :
							aBaseURI.indexOf('file:') == 0 ?
								IOService.newFileURI(FileHandler.getFileFromURLSpec(aURISpec)) :
								IOService.newURI(aURISpec, null, null) ;
				return base.resolve(aURISpec);
			},
			doAndWait : function(aAsyncTask) {
				return doAndWait(aAsyncTask);
			},
			exports : {}
		};
	return ns;
}

function _createFakeLocation(aURI)
{
	aURI = aURI.QueryInterface(Components.interfaces.nsIURL)
					.QueryInterface(Components.interfaces.nsIURI);
	return {
		href     : aURI.spec,
		search   : aURI.query ? '?'+aURI.query : '' ,
		hash     : aURI.ref ? '#'+aURI.ref : '' ,
		host     : aURI.scheme == 'jar' ? '' : aURI.hostPort,
		hostname : aURI.scheme == 'jar' ? '' : aURI.host,
		port     : aURI.scheme == 'jar' ? -1 : aURI.port,
		pathname : aURI.path,
		protocol : aURI.scheme+':',
		reload   : function() {},
		replace  : function() {},
		toString : function() {
			return this.href;
		}
	};
}

function _callHandler(aHandler, aReason)
{
	for (var i in _namespaces)
	{
		try {
			if (_namespaces[i][aHandler] &&
				typeof _namespaces[i][aHandler] == 'function')
				_namespaces[i][aHandler](aReason);
		}
		catch(e) {
			dump(i+'('+aHandler+', '+aReason+')\n'+e+'\n');
		}
	}
}

function registerResource(aName, aRoot)
{
	IOService.getProtocolHandler('resource')
		.QueryInterface(Components.interfaces.nsIResProtocolHandler)
		.setSubstitution(aName, aRoot);
}

function unregisterResource(aName)
{
	IOService.getProtocolHandler('resource')
		.QueryInterface(Components.interfaces.nsIResProtocolHandler)
		.setSubstitution(aName, null);
}

/** Handler for "install" of the bootstrap.js */
function install(aReason)
{
	_callHandler('iustall', aReason);
}

/** Handler for "uninstall" of the bootstrap.js */
function uninstall(aReason)
{
	_callHandler('uninstall', aReason);
}

/** Handler for "shutdown" of the bootstrap.js */
function shutdown(aReason)
{
	_callHandler('shutdown', aReason);

	for each (let ns in _namespaces)
	{
		for (let i in ns.exports)
		{
			if (ns.exports.hasOwnProperty(i))
				delete ns.exports[i];
		}
	}
	_namespaces = void(0);

	IOService = void(0);
	FileHandler = void(0);

	load = void(0);
	_exportSymbols = void(0);
	exists = void(0);
	_createNamespace = void(0);
	_callHandler = void(0);
	registerResource = void(0);
	unregisterResource = void(0);
	install = void(0);
	uninstall = void(0);
	shutdown = void(0);
}
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape, defineMessages, FormattedMessage } from 'react-intl';
import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import Box from '@material-ui/core/Box';
import Button from '@material-ui/core/Button';
import Divider from '@material-ui/core/Divider';
import IconButton from '@material-ui/core/IconButton';
import InputAdornment from '@material-ui/core/InputAdornment';
import Typography from '@material-ui/core/Typography';
import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined';
import EventIcon from '@material-ui/icons/Event';
import ScheduleIcon from '@material-ui/icons/Schedule';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
import Switch from '@material-ui/core/Switch';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import SmoochBotPreviewFeed from './SmoochBotPreviewFeed';
import { placeholders } from './localizables';
import { inProgressYellow, completedGreen, opaqueBlack38, opaqueBlack23 } from '../../../styles/js/shared';
import ParsedText from '../../ParsedText';
import timezones from '../../../timezones';

const useStyles = makeStyles(theme => ({
  spaced: {
    margin: theme.spacing(1),
    minWidth: 100,
  },
  title: {
    margin: theme.spacing(1.25),
  },
  rssPreview: {
    height: 'initial',
    display: 'initial',
    maxHeight: 'initial',
    alignItems: 'initial',
    whiteSpace: 'initial',
    margin: 0,
    width: '100%',
  },
  content: {
    flexWrap: 'wrap',
  },
  icon: {
    color: '#979797',
  },
  load: {
    height: theme.spacing(4),
  },
  divider: {
    marginTop: theme.spacing(2),
    marginBottom: theme.spacing(2),
  },
  schedule: {
    gap: '8px',
  },
  active: {
    background: completedGreen,
    color: 'white',
  },
  paused: {
    background: inProgressYellow,
    color: 'white',
  },
  none: {
    background: '#F6F6F6',
    color: 'black',
  },
  bullet: {
    color: opaqueBlack38,
    fontSize: theme.spacing(2),
    marginRight: theme.spacing(1),
  },
  textField: {
    border: `1px solid ${opaqueBlack23}`,
    borderRadius: theme.spacing(0.5),
  },
  bulletPoints: {
    gap: `${theme.spacing(1)}px`,
    padding: theme.spacing(1),
    flexWrap: 'wrap',
  },
  rssEntry: {
    overflowWrap: 'break-word',
    wordWrap: 'break-word',
    wordBreak: 'break-word',
  },
}));

const messages = defineMessages({
  rssPlaceholder: {
    id: 'smoochBotNewsletterEditor.rssPlaceholder',
    defaultMessage: 'Public URL that returns N articles in RSS format',
  },
});

const SmoochBotNewsletterEditor = ({
  intl,
  installationId,
  newsletter,
  newsletterInformation,
  onChange,
}) => {
  const classes = useStyles();
  const [error, setError] = React.useState(null);
  const [url, setUrl] = React.useState(newsletter.smooch_newsletter_feed_url);
  const [count, setCount] = React.useState(newsletter.smooch_newsletter_number_of_articles);
  const [refetch, setRefetch] = React.useState(1);
  const [loading, setLoading] = React.useState(false);
  const [rssPreview, setRssPreview] = React.useState(null);
  const [rssEnabled, setRssEnabled] = React.useState(Boolean(url));
  const body = newsletter.smooch_newsletter_body || '';
  const bulletPoints = body.split(/\n+/);
  const [numberOfBulletPoints, setNumberOfBulletPoints] = React.useState(bulletPoints.length || 1);

  const handleError = () => {
    setLoading(false);
    setError(<FormattedMessage id="smoochBotNewsletterEditor.error" defaultMessage="This URL does not seem to be a valid RSS feed" />);
  };

  const handleSuccess = (preview) => {
    setRssPreview(preview);
    setLoading(false);
  };

  const handleLoad = () => {
    if (newsletter.smooch_newsletter_feed_url) {
      setRefetch(refetch + 1);
      setError(null);
      setLoading(true);
      setUrl(newsletter.smooch_newsletter_feed_url.trim());
      setCount(newsletter.smooch_newsletter_number_of_articles);
    } else {
      handleError();
    }
  };

  const handleReset = () => {
    setRssPreview(null);
    setUrl('');
    onChange('smooch_newsletter_feed_url', '');
  };

  const handleChangeBulletPoint = (i, text) => {
    if (!bulletPoints[i] || bulletPoints[i] !== text) {
      const newBulletPoints = bulletPoints.slice();
      newBulletPoints[i] = text;
      const newBody = newBulletPoints.join('\n\n');
      onChange('smooch_newsletter_body', newBody);
    }
  };

  const handleChangeNumberOfBulletPoints = (event) => {
    const value = parseInt(event.target.value, 10);
    const newBody = [];
    [...Array(3).keys()].forEach((i) => {
      if (i < value) {
        newBody.push(bulletPoints[i]);
      }
    });
    onChange('smooch_newsletter_body', newBody.join('\n\n'));
    setNumberOfBulletPoints(value);
  };

  const handleToggleRss = (event) => {
    const enabled = event.target.checked;
    setRssEnabled(enabled);
    if (enabled) {
      onChange('smooch_newsletter_body', '');
    } else {
      handleReset();
    }
  };

  return (
    <React.Fragment>
      <Box>
        <Typography variant="subtitle2" component="div">
          <FormattedMessage
            id="smoochBotNewsletterEditor.title"
            defaultMessage="Compose your newsletter"
          />
        </Typography>
        { newsletterInformation ?
          <Box p={1} mt={1} mb={1} className={newsletterInformation.paused ? classes.paused : classes.active}>
            <Typography component="div" variant="body2">
              { newsletterInformation.paused ?
                <FormattedMessage
                  id="smoochBotNewsletterEditor.paused"
                  defaultMessage="To send your next newsletter, please add new content"
                /> :
                <FormattedMessage
                  id="smoochBotNewsletterEditor.active"
                  defaultMessage="The newsletter will be sent to {count} users on {date}, {time}"
                  values={{
                    count: newsletterInformation.subscribers_count,
                    date: newsletterInformation.next_date,
                    time: newsletterInformation.next_time,
                  }}
                /> }
            </Typography>
          </Box> :
          <Box p={1} mt={1} mb={1} className={classes.none}>
            <Typography component="div" variant="body2">
              <FormattedMessage
                id="smoochBotNewsletterEditor.none"
                defaultMessage="Please complete the steps below to send a weekly newsletter"
              />
            </Typography>
          </Box> }
        <Typography component="div" paragraph>

            <FormattedMessage id="smoochBotNewsletterEditor.firstStep" defaultMessage="1. Select a day and time of the week" description="This is an item in a bullet list of steps, this is the first step" />

        </Typography>
      </Box>
      <Box display="flex" justifyContent="flex-start" alignItems="center" mt={1} mb={1} className={classes.schedule}>
        <Typography>
          <FormattedMessage
            id="smoochBotNewsletterEditor.sendEvery"
            defaultMessage="Send every"
            description="After this string, there is a drop-down where the user can choose a day of the week"
          />
        </Typography>
        <Select
          value={newsletter.smooch_newsletter_day || 'none'}
          variant="outlined"
          onChange={(event) => { onChange('smooch_newsletter_day', event.target.value); }}
          startAdornment={
            <InputAdornment position="start">
              <EventIcon />
            </InputAdornment>
          }
        >
          <MenuItem value="none" disabled><FormattedMessage id="smoochBotNewsletterEditor.day" defaultMessage="Day of week" /></MenuItem>
          <MenuItem value="monday"><FormattedMessage id="smoochBotNewsletterEditor.monday" defaultMessage="Monday" /></MenuItem>
          <MenuItem value="tuesday"><FormattedMessage id="smoochBotNewsletterEditor.tuesday" defaultMessage="Tuesday" /></MenuItem>
          <MenuItem value="wednesday"><FormattedMessage id="smoochBotNewsletterEditor.wednesday" defaultMessage="Wednesday" /></MenuItem>
          <MenuItem value="thursday"><FormattedMessage id="smoochBotNewsletterEditor.thursday" defaultMessage="Thursday" /></MenuItem>
          <MenuItem value="friday"><FormattedMessage id="smoochBotNewsletterEditor.friday" defaultMessage="Friday" /></MenuItem>
          <MenuItem value="saturday"><FormattedMessage id="smoochBotNewsletterEditor.saturday" defaultMessage="Saturday" /></MenuItem>
          <MenuItem value="sunday"><FormattedMessage id="smoochBotNewsletterEditor.sunday" defaultMessage="Sunday" /></MenuItem>
        </Select>
        <Typography>
          <FormattedMessage
            id="smoochBotNewsletterAt"
            defaultMessage="at"
            description="The context here is a time... for example, at 10am"
          />
        </Typography>
        <Select
          value={newsletter.smooch_newsletter_time || 'none'}
          variant="outlined"
          onChange={(event) => { onChange('smooch_newsletter_time', event.target.value); }}
          startAdornment={
            <InputAdornment position="start">
              <ScheduleIcon />
            </InputAdornment>
          }
        >
          <MenuItem value="none" disabled><FormattedMessage id="smoochBotNewsletterEditor.time" defaultMessage="Time" /></MenuItem>
          { [...Array(24).keys()].map(hour => <MenuItem key={hour} value={`${hour}`}>{`${hour}:00`}</MenuItem>) }
        </Select>
        <Select
          value={newsletter.smooch_newsletter_timezone || 'none'}
          variant="outlined"
          onChange={(event) => { onChange('smooch_newsletter_timezone', event.target.value); }}
        >
          <MenuItem value="none" disabled><FormattedMessage id="smoochBotNewsletterEditor.timezone" defaultMessage="Timezone" /></MenuItem>
          { Object.keys(timezones).sort().map(timezone => <MenuItem key={timezone} value={timezone}>{timezone}</MenuItem>) }
        </Select>
      </Box>
      <Box>
        <Typography component="div" paragraph>

            <FormattedMessage id="smoochBotNewsletterEditor.secondStep" defaultMessage="2. Add content manually, or via RSS Feed." description="This is an item in a bullet list of steps, this is the second step" />

          {' '}
          <FormattedMessage id="smoochBotNewsletterEditor.secondStep2" defaultMessage="If the content is not changed between two scheduled sendouts, it will not be sent." />
        </Typography>
      </Box>
      <Box mb={3}>
        <FormControlLabel
          control={
            <Switch
              checked={rssEnabled}
              onChange={handleToggleRss}
            />
          }
          label={
            <FormattedMessage
              id="smoochBotNewsletterEditor.toggleRss"
              defaultMessage="Automatically update content using an RSS feed"
            />
          }
        />
      </Box>

      { !rssEnabled ?
        <Box>
          <TextField
            key={Math.random().toString().substring(2, 10)}
            type="number"
            label={
              <FormattedMessage
                id="smoochBotNewsletterEditor.numberOfBulletPoints"
                defaultMessage="Number of bullet points"
              />
            }
            defaultValue={numberOfBulletPoints}
            onChange={handleChangeNumberOfBulletPoints}
            inputProps={{ step: 1, min: 1, max: 3 }}
            variant="outlined"
            fullWidth
          />
          <Box mt={1} mb={1} className={classes.textField}>
            <Box p={1}>
              <Typography>
                <FormattedMessage
                  id="smoochBotNewsletterEditor.templateHeaderNoRss"
                  defaultMessage="You are receiving this message because you opted to receive our 'Weekly COVID-19 Facts' Newsletter. Here is the most important information for the week of {date}: "
                  values={{
                    date: new Date().toLocaleString(intl.locale, { month: 'short', day: '2-digit' }),
                  }}
                />
              </Typography>
            </Box>
            <Divider />
            <Box display="flex" className={classes.bulletPoints} width={1}>
              { [...Array(numberOfBulletPoints).keys()].map((value, i) => (
                <Box display="flex" alignItems="center" width={1} key={value}>
                  { numberOfBulletPoints > 1 ?
                    <Box className={classes.bullet}>
                      ●
                    </Box> : null }
                  <TextField
                    key={Math.random().toString().substring(2, 10)}
                    placeholder={intl.formatMessage(placeholders.smooch_newsletter_bullet_point)}
                    variant="outlined"
                    defaultValue={bulletPoints[i]}
                    onBlur={(event) => { handleChangeBulletPoint(i, event.target.value); }}
                    fullWidth
                  />
                </Box>
              ))}
            </Box>
          </Box>
        </Box> : null }

      { rssEnabled ?
        <Box>
          <Box display="flex" justifyContent="space-between" alignItems={error ? 'baseline' : 'center'}>
            <TextField
              key={Math.random().toString().substring(2, 10)}
              label={
                <FormattedMessage
                  id="smoochBotNewsletterEditor.url"
                  defaultMessage="URL"
                />
              }
              placeholder={intl.formatMessage(messages.rssPlaceholder)}
              className={classes.spaced}
              defaultValue={newsletter.smooch_newsletter_feed_url}
              onBlur={(event) => {
                onChange('smooch_newsletter_feed_url', event.target.value.trim());
              }}
              error={Boolean(error)}
              helperText={error}
              variant="outlined"
              fullWidth
            />

            <TextField
              key={Math.random().toString().substring(2, 10)}
              type="number"
              label={
                <FormattedMessage
                  id="smoochBotNewsletterEditor.numberOfArticles"
                  defaultMessage="Number of articles to return"
                />
              }
              className={classes.spaced}
              defaultValue={newsletter.smooch_newsletter_number_of_articles || 0}
              onBlur={(event) => {
                onChange('smooch_newsletter_number_of_articles', parseInt(event.target.value, 10));
              }}
              inputProps={{ step: 1, min: 1, max: 3 }}
              variant="outlined"
              fullWidth
            />

            <Button variant="contained" color="primary" className={classes.spaced} onClick={handleLoad} disabled={loading || !newsletter.smooch_newsletter_feed_url}>
              <FormattedMessage
                id="smoochBotNewsletterEditor.load"
                defaultMessage="Load"
              />
            </Button>

            <IconButton onClick={handleReset}>
              <CancelOutlinedIcon className={classes.icon} />
            </IconButton>
          </Box>
          <Box display="flex" flexWrap="wrap">
            { rssPreview ?
              <Box mt={1} mb={1} className={classes.textField}>
                <Box p={1}>
                  <Typography>
                    <FormattedMessage
                      id="smoochBotNewsletterEditor.templateHeaderNoRss"
                      defaultMessage="You are receiving this message because you opted to receive our 'Weekly COVID-19 Facts' Newsletter. Here is the most important information for the week of {date}: "
                      values={{
                        date: new Date().toLocaleString(intl.locale, { month: 'short', day: '2-digit' }),
                      }}
                    />
                  </Typography>
                </Box>
                <Divider />
                <Box display="flex" className={classes.bulletPoints} width={1}>
                  { rssPreview.split('\n\n').map(entry => (
                    <Box display="flex" alignItems="start" width={1} key={entry}>
                      { rssPreview.split('\n\n').length > 1 ?
                        <Box className={classes.bullet}>
                          ●
                        </Box> : null }
                      <Typography className={classes.rssEntry}>
                        <ParsedText text={entry} />
                      </Typography>
                    </Box>
                  ))}
                </Box>
              </Box> : null }
            { url && count && !error ?
              <SmoochBotPreviewFeed
                installationId={installationId}
                feedUrl={url}
                count={count}
                refetch={refetch}
                onError={handleError}
                onSuccess={handleSuccess}
              /> : null }
          </Box>
        </Box> : null }
    </React.Fragment>
  );
};

SmoochBotNewsletterEditor.propTypes = {
  installationId: PropTypes.string.isRequired,
  newsletter: PropTypes.object.isRequired,
  newsletterInformation: PropTypes.object.isRequired,
  onChange: PropTypes.func.isRequired,
  intl: intlShape.isRequired,
};

export default injectIntl(SmoochBotNewsletterEditor);
<commit_msg>Ticket CHECK-923: Fixing minor CSS issue<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape, defineMessages, FormattedMessage } from 'react-intl';
import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import Box from '@material-ui/core/Box';
import Button from '@material-ui/core/Button';
import Divider from '@material-ui/core/Divider';
import IconButton from '@material-ui/core/IconButton';
import InputAdornment from '@material-ui/core/InputAdornment';
import Typography from '@material-ui/core/Typography';
import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined';
import EventIcon from '@material-ui/icons/Event';
import ScheduleIcon from '@material-ui/icons/Schedule';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
import Switch from '@material-ui/core/Switch';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import SmoochBotPreviewFeed from './SmoochBotPreviewFeed';
import { placeholders } from './localizables';
import { inProgressYellow, completedGreen, opaqueBlack38, opaqueBlack23 } from '../../../styles/js/shared';
import ParsedText from '../../ParsedText';
import timezones from '../../../timezones';

const useStyles = makeStyles(theme => ({
  spaced: {
    margin: theme.spacing(1),
    minWidth: 100,
  },
  title: {
    margin: theme.spacing(1.25),
  },
  rssPreview: {
    height: 'initial',
    display: 'initial',
    maxHeight: 'initial',
    alignItems: 'initial',
    whiteSpace: 'initial',
    margin: 0,
    width: '100%',
  },
  content: {
    flexWrap: 'wrap',
  },
  icon: {
    color: '#979797',
  },
  load: {
    height: theme.spacing(4),
  },
  divider: {
    marginTop: theme.spacing(2),
    marginBottom: theme.spacing(2),
  },
  schedule: {
    gap: '8px',
  },
  active: {
    background: completedGreen,
    color: 'white',
  },
  paused: {
    background: inProgressYellow,
    color: 'white',
  },
  none: {
    background: '#F6F6F6',
    color: 'black',
  },
  bullet: {
    color: opaqueBlack38,
    fontSize: theme.spacing(2),
    marginRight: theme.spacing(1),
    height: theme.spacing(7),
    alignItems: 'center',
    display: 'flex',
  },
  bulletRss: {
    color: opaqueBlack38,
    fontSize: theme.spacing(2),
    marginRight: theme.spacing(1),
  },
  textField: {
    border: `1px solid ${opaqueBlack23}`,
    borderRadius: theme.spacing(0.5),
  },
  bulletPoints: {
    gap: `${theme.spacing(1)}px`,
    padding: theme.spacing(1),
    flexWrap: 'wrap',
  },
  rssEntry: {
    overflowWrap: 'break-word',
    wordWrap: 'break-word',
    wordBreak: 'break-word',
  },
}));

const messages = defineMessages({
  rssPlaceholder: {
    id: 'smoochBotNewsletterEditor.rssPlaceholder',
    defaultMessage: 'Public URL that returns N articles in RSS format',
  },
});

const SmoochBotNewsletterEditor = ({
  intl,
  installationId,
  newsletter,
  newsletterInformation,
  onChange,
}) => {
  const classes = useStyles();
  const [error, setError] = React.useState(null);
  const [url, setUrl] = React.useState(newsletter.smooch_newsletter_feed_url);
  const [count, setCount] = React.useState(newsletter.smooch_newsletter_number_of_articles);
  const [refetch, setRefetch] = React.useState(1);
  const [loading, setLoading] = React.useState(false);
  const [rssPreview, setRssPreview] = React.useState(null);
  const [rssEnabled, setRssEnabled] = React.useState(Boolean(url));
  const body = newsletter.smooch_newsletter_body || '';
  const bulletPoints = body.split(/\n+/);
  const [numberOfBulletPoints, setNumberOfBulletPoints] = React.useState(bulletPoints.length || 1);

  const handleError = () => {
    setLoading(false);
    setError(<FormattedMessage id="smoochBotNewsletterEditor.error" defaultMessage="This URL does not seem to be a valid RSS feed" />);
  };

  const handleSuccess = (preview) => {
    setRssPreview(preview);
    setLoading(false);
  };

  const handleLoad = () => {
    if (newsletter.smooch_newsletter_feed_url) {
      setRefetch(refetch + 1);
      setError(null);
      setLoading(true);
      setUrl(newsletter.smooch_newsletter_feed_url.trim());
      setCount(newsletter.smooch_newsletter_number_of_articles);
    } else {
      handleError();
    }
  };

  const handleReset = () => {
    setRssPreview(null);
    setUrl('');
    onChange('smooch_newsletter_feed_url', '');
  };

  const handleChangeBulletPoint = (i, text) => {
    if (!bulletPoints[i] || bulletPoints[i] !== text) {
      const newBulletPoints = bulletPoints.slice();
      newBulletPoints[i] = text.replaceAll('\n', ' ');
      const newBody = newBulletPoints.join('\n\n');
      onChange('smooch_newsletter_body', newBody);
    }
  };

  const handleChangeNumberOfBulletPoints = (event) => {
    const value = parseInt(event.target.value, 10);
    const newBody = [];
    [...Array(3).keys()].forEach((i) => {
      if (i < value) {
        newBody.push(bulletPoints[i]);
      }
    });
    onChange('smooch_newsletter_body', newBody.join('\n\n'));
    setNumberOfBulletPoints(value);
  };

  const handleToggleRss = (event) => {
    const enabled = event.target.checked;
    setRssEnabled(enabled);
    if (enabled) {
      onChange('smooch_newsletter_body', '');
    } else {
      handleReset();
    }
  };

  return (
    <React.Fragment>
      <Box>
        <Typography variant="subtitle2" component="div">
          <FormattedMessage
            id="smoochBotNewsletterEditor.title"
            defaultMessage="Compose your newsletter"
          />
        </Typography>
        { newsletterInformation ?
          <Box p={1} mt={1} mb={1} className={newsletterInformation.paused ? classes.paused : classes.active}>
            <Typography component="div" variant="body2">
              { newsletterInformation.paused ?
                <FormattedMessage
                  id="smoochBotNewsletterEditor.paused"
                  defaultMessage="To send your next newsletter, please add new content"
                /> :
                <FormattedMessage
                  id="smoochBotNewsletterEditor.active"
                  defaultMessage="The newsletter will be sent to {count} users on {date}, {time}"
                  values={{
                    count: newsletterInformation.subscribers_count,
                    date: newsletterInformation.next_date,
                    time: newsletterInformation.next_time,
                  }}
                /> }
            </Typography>
          </Box> :
          <Box p={1} mt={1} mb={1} className={classes.none}>
            <Typography component="div" variant="body2">
              <FormattedMessage
                id="smoochBotNewsletterEditor.none"
                defaultMessage="Please complete the steps below to send a weekly newsletter"
              />
            </Typography>
          </Box> }
        <Typography component="div" paragraph>

            <FormattedMessage id="smoochBotNewsletterEditor.firstStep" defaultMessage="1. Select a day and time of the week" description="This is an item in a bullet list of steps, this is the first step" />

        </Typography>
      </Box>
      <Box display="flex" justifyContent="flex-start" alignItems="center" mt={1} mb={1} className={classes.schedule}>
        <Typography>
          <FormattedMessage
            id="smoochBotNewsletterEditor.sendEvery"
            defaultMessage="Send every"
            description="After this string, there is a drop-down where the user can choose a day of the week"
          />
        </Typography>
        <Select
          value={newsletter.smooch_newsletter_day || 'none'}
          variant="outlined"
          onChange={(event) => { onChange('smooch_newsletter_day', event.target.value); }}
          startAdornment={
            <InputAdornment position="start">
              <EventIcon />
            </InputAdornment>
          }
        >
          <MenuItem value="none" disabled><FormattedMessage id="smoochBotNewsletterEditor.day" defaultMessage="Day of week" /></MenuItem>
          <MenuItem value="monday"><FormattedMessage id="smoochBotNewsletterEditor.monday" defaultMessage="Monday" /></MenuItem>
          <MenuItem value="tuesday"><FormattedMessage id="smoochBotNewsletterEditor.tuesday" defaultMessage="Tuesday" /></MenuItem>
          <MenuItem value="wednesday"><FormattedMessage id="smoochBotNewsletterEditor.wednesday" defaultMessage="Wednesday" /></MenuItem>
          <MenuItem value="thursday"><FormattedMessage id="smoochBotNewsletterEditor.thursday" defaultMessage="Thursday" /></MenuItem>
          <MenuItem value="friday"><FormattedMessage id="smoochBotNewsletterEditor.friday" defaultMessage="Friday" /></MenuItem>
          <MenuItem value="saturday"><FormattedMessage id="smoochBotNewsletterEditor.saturday" defaultMessage="Saturday" /></MenuItem>
          <MenuItem value="sunday"><FormattedMessage id="smoochBotNewsletterEditor.sunday" defaultMessage="Sunday" /></MenuItem>
        </Select>
        <Typography>
          <FormattedMessage
            id="smoochBotNewsletterAt"
            defaultMessage="at"
            description="The context here is a time... for example, at 10am"
          />
        </Typography>
        <Select
          value={newsletter.smooch_newsletter_time || 'none'}
          variant="outlined"
          onChange={(event) => { onChange('smooch_newsletter_time', event.target.value); }}
          startAdornment={
            <InputAdornment position="start">
              <ScheduleIcon />
            </InputAdornment>
          }
        >
          <MenuItem value="none" disabled><FormattedMessage id="smoochBotNewsletterEditor.time" defaultMessage="Time" /></MenuItem>
          { [...Array(24).keys()].map(hour => <MenuItem key={hour} value={`${hour}`}>{`${hour}:00`}</MenuItem>) }
        </Select>
        <Select
          value={newsletter.smooch_newsletter_timezone || 'none'}
          variant="outlined"
          onChange={(event) => { onChange('smooch_newsletter_timezone', event.target.value); }}
        >
          <MenuItem value="none" disabled><FormattedMessage id="smoochBotNewsletterEditor.timezone" defaultMessage="Timezone" /></MenuItem>
          { Object.keys(timezones).sort().map(timezone => <MenuItem key={timezone} value={timezone}>{timezone}</MenuItem>) }
        </Select>
      </Box>
      <Box>
        <Typography component="div" paragraph>

            <FormattedMessage id="smoochBotNewsletterEditor.secondStep" defaultMessage="2. Add content manually, or via RSS Feed." description="This is an item in a bullet list of steps, this is the second step" />

          {' '}
          <FormattedMessage id="smoochBotNewsletterEditor.secondStep2" defaultMessage="If the content is not changed between two scheduled sendouts, it will not be sent." />
        </Typography>
      </Box>
      <Box mb={3}>
        <FormControlLabel
          control={
            <Switch
              checked={rssEnabled}
              onChange={handleToggleRss}
            />
          }
          label={
            <FormattedMessage
              id="smoochBotNewsletterEditor.toggleRss"
              defaultMessage="Automatically update content using an RSS feed"
            />
          }
        />
      </Box>

      { !rssEnabled ?
        <Box>
          <TextField
            key={Math.random().toString().substring(2, 10)}
            type="number"
            label={
              <FormattedMessage
                id="smoochBotNewsletterEditor.numberOfBulletPoints"
                defaultMessage="Number of bullet points"
              />
            }
            defaultValue={numberOfBulletPoints}
            onChange={handleChangeNumberOfBulletPoints}
            inputProps={{ step: 1, min: 1, max: 3 }}
            variant="outlined"
            fullWidth
          />
          <Box mt={1} mb={1} className={classes.textField}>
            <Box p={1}>
              <Typography>
                <FormattedMessage
                  id="smoochBotNewsletterEditor.templateHeaderNoRss"
                  defaultMessage="You are receiving this message because you opted to receive our 'Weekly COVID-19 Facts' Newsletter. Here is the most important information for the week of {date}: "
                  values={{
                    date: new Date().toLocaleString(intl.locale, { month: 'short', day: '2-digit' }),
                  }}
                />
              </Typography>
            </Box>
            <Divider />
            <Box display="flex" className={classes.bulletPoints} width={1}>
              { [...Array(numberOfBulletPoints).keys()].map((value, i) => (
                <Box display="flex" alignItems="start" width={1} key={value}>
                  { numberOfBulletPoints > 1 ?
                    <Box className={classes.bullet}>
                      ●
                    </Box> : null }
                  <TextField
                    key={Math.random().toString().substring(2, 10)}
                    placeholder={intl.formatMessage(placeholders.smooch_newsletter_bullet_point)}
                    variant="outlined"
                    defaultValue={bulletPoints[i]}
                    onBlur={(event) => { handleChangeBulletPoint(i, event.target.value); }}
                    rows="1"
                    rowsMax={Infinity}
                    multiline
                    fullWidth
                  />
                </Box>
              ))}
            </Box>
          </Box>
        </Box> : null }

      { rssEnabled ?
        <Box>
          <Box display="flex" justifyContent="space-between" alignItems={error ? 'baseline' : 'center'}>
            <TextField
              key={Math.random().toString().substring(2, 10)}
              label={
                <FormattedMessage
                  id="smoochBotNewsletterEditor.url"
                  defaultMessage="URL"
                />
              }
              placeholder={intl.formatMessage(messages.rssPlaceholder)}
              className={classes.spaced}
              defaultValue={newsletter.smooch_newsletter_feed_url}
              onBlur={(event) => {
                onChange('smooch_newsletter_feed_url', event.target.value.trim());
              }}
              error={Boolean(error)}
              helperText={error}
              variant="outlined"
              fullWidth
            />

            <TextField
              key={Math.random().toString().substring(2, 10)}
              type="number"
              label={
                <FormattedMessage
                  id="smoochBotNewsletterEditor.numberOfArticles"
                  defaultMessage="Number of articles to return"
                />
              }
              className={classes.spaced}
              defaultValue={newsletter.smooch_newsletter_number_of_articles || 0}
              onBlur={(event) => {
                onChange('smooch_newsletter_number_of_articles', parseInt(event.target.value, 10));
              }}
              inputProps={{ step: 1, min: 1, max: 3 }}
              variant="outlined"
              fullWidth
            />

            <Button variant="contained" color="primary" className={classes.spaced} onClick={handleLoad} disabled={loading || !newsletter.smooch_newsletter_feed_url}>
              <FormattedMessage
                id="smoochBotNewsletterEditor.load"
                defaultMessage="Load"
              />
            </Button>

            <IconButton onClick={handleReset}>
              <CancelOutlinedIcon className={classes.icon} />
            </IconButton>
          </Box>
          <Box display="flex" flexWrap="wrap">
            { rssPreview ?
              <Box mt={1} mb={1} className={classes.textField}>
                <Box p={1}>
                  <Typography>
                    <FormattedMessage
                      id="smoochBotNewsletterEditor.templateHeaderNoRss"
                      defaultMessage="You are receiving this message because you opted to receive our 'Weekly COVID-19 Facts' Newsletter. Here is the most important information for the week of {date}: "
                      values={{
                        date: new Date().toLocaleString(intl.locale, { month: 'short', day: '2-digit' }),
                      }}
                    />
                  </Typography>
                </Box>
                <Divider />
                <Box display="flex" className={classes.bulletPoints} width={1}>
                  { rssPreview.split('\n\n').map(entry => (
                    <Box display="flex" alignItems="start" width={1} key={entry}>
                      { rssPreview.split('\n\n').length > 1 ?
                        <Box className={classes.bulletRss}>
                          ●
                        </Box> : null }
                      <Typography className={classes.rssEntry}>
                        <ParsedText text={entry} />
                      </Typography>
                    </Box>
                  ))}
                </Box>
              </Box> : null }
            { url && count && !error ?
              <SmoochBotPreviewFeed
                installationId={installationId}
                feedUrl={url}
                count={count}
                refetch={refetch}
                onError={handleError}
                onSuccess={handleSuccess}
              /> : null }
          </Box>
        </Box> : null }
    </React.Fragment>
  );
};

SmoochBotNewsletterEditor.defaultProps = {
  newsletterInformation: null,
};

SmoochBotNewsletterEditor.propTypes = {
  installationId: PropTypes.string.isRequired,
  newsletter: PropTypes.object.isRequired,
  onChange: PropTypes.func.isRequired,
  intl: intlShape.isRequired,
  newsletterInformation: PropTypes.object,
};

export default injectIntl(SmoochBotNewsletterEditor);
<|endoftext|>


<commit_before>import { ArchmageSocket } from './archmage-socket';
import Promise from 'bluebird';
import crypto from 'crypto';

// const defaults = {
//   confAPIName: 'xiconf',
//   readUserSignal: 'readUsers',
//   confUpdateSignal: 'confUpdate',
// };

export default class ArchmageSession {

  // ------ SETUP ------ //

  constructor(url, protocols, options = {}) {
    this.authenticated = false;
    this.hasBeenConnected = false;
    this.authObj = undefined;
    this.user = undefined;
    if (url) {
      this.connect(url, protocols, options);
    } else {
      this.setOptions(options);
    }
  }

  connect(url, protocols, options = {}) {
    this.setOptions(options);
    this.socket = new ArchmageSocket(url, protocols, options);
    this.socket.ws.onOpen(this.onOpen);
    this.socket.ws.onClose(this.onClose);
  }

  setOptions(options) {
    this.reloginCallback = options.reloginCallback || this.reloginCallback;
    this.reloginFailCallback = options.reloginFailCallback || this.reloginFailCallback;
    this.userObjUpdateCallback = options.userObjUpdateCallback || this.userObjUpdateCallback;
    this.confAPIName = options.confAPIName || this.confAPIName;
    this.readUserSignal = options.readUserSignal || this.readUserSignal;
    this.confUpdateSignal = options.confUpdateSignal || this.confUpdateSignal;
  }

  // ------ INTERFACE IMPLEMENTATION ------ //

  isOpen() {
    if (this.socket) {
      return this.socket.isOpen() && this.authenticated;
    }
    return false;
  }

  auth(userId, password, tenant, target, signal, source, payloadExtra) {
    const passwordHash = this.hashify(password);
    const reqInitObj = Map({
      userId, passwordHash, tenant, target, signal, source, payloadExtra,
    });
    return this.socket.init(userId, passwordHash, tenant, target, signal, source, payloadExtra)
      .then(msgObj => {
        if (!this.handleInitReply(msgObj, reqInitObj)) {
          const reason = `${msgObj.signal}: ${msgObj.payload && msgObj.payload[0]}`;
          Promise.reject(reason);
        }
        return msgObj;
      });
  }

  logout() {
    this.user = undefined;
    this.authenticated = false;
    this.authObj = undefined;
    const tempResult = this.socket.kill(true);
    this.socket = undefined;
    return tempResult;
  }

  // BELOW NEEDS CONVERSION IF ACTIVATED!
  // login(userId, password, tenant, pid, signal, source, payloadExtra) {
  //   const passwordHash = this.hashify(password);
  //   const defer = getDefer();
  //
  //   this.socket.init(userId, passwordHash, tenant, pid, signal, source, payloadExtra)
  //     .then(initSuccess.bind(this));
  //
  //   return defer.promise;
  //
  //   // //////
  //
  //   function initSuccess(msgObj) {
  //     if (this.handleInitReply(msgObj, userId, passwordHash)) {
  //       if (this.authObj.rid) {
  //         this.readUser(this.authObj.rid)
  //           .then(defer.resolve, defer.reject);
  //       } else {
  //         defer.reject('No rid for user object: '+this.authObj.rid);
  //       }
  //     } else {
  //       defer.reject(msgObj.signal+': '+(msgObj.payload ? msgObj.payload[0]+'' : 'undefined'));
  //     }
  //   }
  // }
  // readUser(rid, pid, signal) {
  //   const defer = getDefer();
  //
  //   pid = pid || this.confAPIName || defaults.confAPIName;
  //   signal = signal || this.readUserSignal || defaults.readUserSignal;
  //   this.socket.req(pid, signal, {rids:[rid]})
  //     .then(reqSuccess.bind(this), defer.reject);
  //
  //   return defer.promise;
  //
  //   // //////
  //
  //   function subSuccess(msgObj) {
  //     if (msgObj.ok) {
  //       defer.resolve(this);  // this is set to msgObj on previous call
  //     } else {
  //       defer.reject(
  //       'sub.'+msgObj.signal+': '+(msgObj.payload ? msgObj.payload[0]+'' : 'undefined')
  //       );
  //     }
  //   }
  //
  //   function reqSuccess(msgObj) {
  //     if (msgObj.ok && msgObj.payload) {
  //       this.user = msgObj.payload[0];
  //       const signal = this.confUpdateSignal || defaults.confUpdateSignal;
  //       this.socket.sub(this.userObjUpdate, signal, [rid])
  //         .then(subSuccess.bind(msgObj), defer.reject);
  //     } else {
  //       defer.reject(
  //       'req.'+msgObj.signal+': '+(msgObj.payload ? msgObj.payload[0]+'' : 'undefined')
  //       );
  //     }
  //   }
  // }

  // ------ PRIVATE METHODS ------ //

  hashify(phrase) {
    return crypto.createHash('sha256').update(phrase).digest('hex');
  }

  handleInitReply(msgObj, reqInitObj) {
    console.log('Login reply: ', msgObj);
    this.authenticated = msgObj.ok;

    if (msgObj.ok) {
      this.authObj = reqInitObj.set('rid', undefined);
      if (msgObj.payload && msgObj.payload[0]) {
        this.authObj = reqInitObj.set('rid', msgObj.payload[0]);
      }
    }
    return msgObj.ok;
  }

  // BELOW NEEDS CONVERSION!
  // userObjUpdate(msgObj) {
  //   if (msgObj.payload && _.isObject(msgObj.payload[0])) {
  //     this.user = msgObj.payload[0];
  //     if (_.isFunction(this.userObjUpdateCallback)) this.userObjUpdateCallback(msgObj);
  //   }
  // }

  onOpen() {
    if (this.hasBeenConnected && this.authObj) {  // Need to relogin?
      this.socket.init(
        this.authObj.get('userId'),
        this.authObj.get('passwordHash'),
        this.authObj.get('tenant'),
        this.authObj.get('target'),
        this.authObj.get('signal'),
        this.authObj.get('source'),
        this.authObj.get('payloadExtra'),
      )
        .then(msgObj => {
          this.authenticated = msgObj.ok;
          console.log('Re-login attempt was successful');
          if (this.reloginCallback) this.reloginCallback(msgObj);
        })
        .catch(reason => {
          console.log('Re-login attempt failed: ', reason);
          if (this.reloginFailCallback) this.reloginFailCallback(reason);
        });
    }
  }

  onClose() {
    this.hasBeenConnected = true;
    this.authenticated = false;
  }
}

// import SHA from 'sha.js';
  // var hashObj:jsSHA.jsSHA = new jsSHA(phrase, 'TEXT');
  // return hashObj.getHash('SHA-256', 'HEX');

  // const sha256 = SHA('sha256');
  // return sha256.update(phrase, 'utf8').digest('hex');
<commit_msg>bind to this fixes<commit_after>import { ArchmageSocket } from './archmage-socket';
import Promise from 'bluebird';
import crypto from 'crypto';

// const defaults = {
//   confAPIName: 'xiconf',
//   readUserSignal: 'readUsers',
//   confUpdateSignal: 'confUpdate',
// };

export default class ArchmageSession {

  // ------ SETUP ------ //

  constructor(url, protocols, options = {}) {
    this.authenticated = false;
    this.hasBeenConnected = false;
    this.authObj = undefined;
    this.user = undefined;
    if (url) {
      this.connect(url, protocols, options);
    } else {
      this.setOptions(options);
    }
  }

  connect(url, protocols, options = {}) {
    this.setOptions(options);
    this.socket = new ArchmageSocket(url, protocols, options);
    this.socket.ws.onOpen(::this.onOpen);
    this.socket.ws.onClose(::this.onClose);
  }

  setOptions(options) {
    this.reloginCallback = options.reloginCallback || this.reloginCallback;
    this.reloginFailCallback = options.reloginFailCallback || this.reloginFailCallback;
    this.userObjUpdateCallback = options.userObjUpdateCallback || this.userObjUpdateCallback;
    this.confAPIName = options.confAPIName || this.confAPIName;
    this.readUserSignal = options.readUserSignal || this.readUserSignal;
    this.confUpdateSignal = options.confUpdateSignal || this.confUpdateSignal;
  }

  // ------ INTERFACE IMPLEMENTATION ------ //

  isOpen() {
    if (this.socket) {
      return this.socket.isOpen() && this.authenticated;
    }
    return false;
  }

  auth(userId, password, tenant, target, signal, source, payloadExtra) {
    const passwordHash = this.hashify(password);
    const reqInitObj = Map({
      userId, passwordHash, tenant, target, signal, source, payloadExtra,
    });
    return this.socket.init(userId, passwordHash, tenant, target, signal, source, payloadExtra)
      .then(msgObj => {
        if (!this.handleInitReply(msgObj, reqInitObj)) {
          const reason = `${msgObj.signal}: ${msgObj.payload && msgObj.payload[0]}`;
          Promise.reject(reason);
        }
        return msgObj;
      });
  }

  logout() {
    this.user = undefined;
    this.authenticated = false;
    this.authObj = undefined;
    const tempResult = this.socket.kill(true);
    this.socket = undefined;
    return tempResult;
  }

  // BELOW NEEDS CONVERSION IF ACTIVATED!
  // login(userId, password, tenant, pid, signal, source, payloadExtra) {
  //   const passwordHash = this.hashify(password);
  //   const defer = getDefer();
  //
  //   this.socket.init(userId, passwordHash, tenant, pid, signal, source, payloadExtra)
  //     .then(initSuccess.bind(this));
  //
  //   return defer.promise;
  //
  //   // //////
  //
  //   function initSuccess(msgObj) {
  //     if (this.handleInitReply(msgObj, userId, passwordHash)) {
  //       if (this.authObj.rid) {
  //         this.readUser(this.authObj.rid)
  //           .then(defer.resolve, defer.reject);
  //       } else {
  //         defer.reject('No rid for user object: '+this.authObj.rid);
  //       }
  //     } else {
  //       defer.reject(msgObj.signal+': '+(msgObj.payload ? msgObj.payload[0]+'' : 'undefined'));
  //     }
  //   }
  // }
  // readUser(rid, pid, signal) {
  //   const defer = getDefer();
  //
  //   pid = pid || this.confAPIName || defaults.confAPIName;
  //   signal = signal || this.readUserSignal || defaults.readUserSignal;
  //   this.socket.req(pid, signal, {rids:[rid]})
  //     .then(reqSuccess.bind(this), defer.reject);
  //
  //   return defer.promise;
  //
  //   // //////
  //
  //   function subSuccess(msgObj) {
  //     if (msgObj.ok) {
  //       defer.resolve(this);  // this is set to msgObj on previous call
  //     } else {
  //       defer.reject(
  //       'sub.'+msgObj.signal+': '+(msgObj.payload ? msgObj.payload[0]+'' : 'undefined')
  //       );
  //     }
  //   }
  //
  //   function reqSuccess(msgObj) {
  //     if (msgObj.ok && msgObj.payload) {
  //       this.user = msgObj.payload[0];
  //       const signal = this.confUpdateSignal || defaults.confUpdateSignal;
  //       this.socket.sub(this.userObjUpdate, signal, [rid])
  //         .then(subSuccess.bind(msgObj), defer.reject);
  //     } else {
  //       defer.reject(
  //       'req.'+msgObj.signal+': '+(msgObj.payload ? msgObj.payload[0]+'' : 'undefined')
  //       );
  //     }
  //   }
  // }

  // ------ PRIVATE METHODS ------ //

  hashify(phrase) {
    return crypto.createHash('sha256').update(phrase).digest('hex');
  }

  handleInitReply(msgObj, reqInitObj) {
    console.log('Login reply: ', msgObj);
    this.authenticated = msgObj.ok;

    if (msgObj.ok) {
      this.authObj = reqInitObj.set('rid', undefined);
      if (msgObj.payload && msgObj.payload[0]) {
        this.authObj = reqInitObj.set('rid', msgObj.payload[0]);
      }
    }
    return msgObj.ok;
  }

  // BELOW NEEDS CONVERSION!
  // userObjUpdate(msgObj) {
  //   if (msgObj.payload && _.isObject(msgObj.payload[0])) {
  //     this.user = msgObj.payload[0];
  //     if (_.isFunction(this.userObjUpdateCallback)) this.userObjUpdateCallback(msgObj);
  //   }
  // }

  onOpen() {
    if (this.hasBeenConnected && this.authObj) {  // Need to relogin?
      this.socket.init(
        this.authObj.get('userId'),
        this.authObj.get('passwordHash'),
        this.authObj.get('tenant'),
        this.authObj.get('target'),
        this.authObj.get('signal'),
        this.authObj.get('source'),
        this.authObj.get('payloadExtra'),
      )
        .then(msgObj => {
          this.authenticated = msgObj.ok;
          console.log('Re-login attempt was successful');
          if (this.reloginCallback) this.reloginCallback(msgObj);
        })
        .catch(reason => {
          console.log('Re-login attempt failed: ', reason);
          if (this.reloginFailCallback) this.reloginFailCallback(reason);
        });
    }
  }

  onClose() {
    this.hasBeenConnected = true;
    this.authenticated = false;
  }
}

// import SHA from 'sha.js';
  // var hashObj:jsSHA.jsSHA = new jsSHA(phrase, 'TEXT');
  // return hashObj.getHash('SHA-256', 'HEX');

  // const sha256 = SHA('sha256');
  // return sha256.update(phrase, 'utf8').digest('hex');
<|endoftext|>