<commit_before>/*
 * Copyright (C) 2012 Thiago Bellini <hackedbellini@gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Most of this code was forked from media-player-indicator:
 *   https://extensions.gnome.org/extension/55/media-player-indicator/
 *
 */

const GObject = imports.gi.GObject;
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;

const Gettext = imports.gettext.domain('gnome-shell-notifications-alert');
const _ = Gettext.gettext;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Lib = Me.imports.lib;

const SETTING_BLACKLIST = 'application-list';
const SETTING_FILTER_TYPE = 'filter';

const Columns = {
  APPINFO: 0,
  DISPLAY_NAME: 1,
  ICON: 2,
};

let settings;
let boolSettings;
let intSettings;
let colorSettings;

function _createBoolSetting(setting) {
  let hbox = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL});

  let settingLabel = new Gtk.Label({label: boolSettings[setting].label,
                                    xalign: 0});

  let settingSwitch = new Gtk.Switch({active: settings.get_boolean(setting)});
  settingSwitch.connect('notify::active', function(button) {
    settings.set_boolean(setting, button.active);
  });

  if (boolSettings[setting].help) {
    settingLabel.set_tooltip_text(boolSettings[setting].help);
    settingSwitch.set_tooltip_text(boolSettings[setting].help);
  }

  hbox.pack_start(settingLabel, true, true, 0);
  hbox.add(settingSwitch);

  return hbox;
}

function _createIntSetting(setting) {
  let hbox = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL});

  let settingLabel = new Gtk.Label({label: intSettings[setting].label,
                                    xalign: 0});

  let spinButton = Gtk.SpinButton.new_with_range(
    intSettings[setting].min,
    intSettings[setting].max,
    intSettings[setting].step)
  spinButton.set_value(settings.get_int(setting));
  spinButton.connect('notify::value', function(spin) {
    settings.set_int(setting, spin.get_value_as_int());
  });

  if (intSettings[setting].help) {
    settingLabel.set_tooltip_text(intSettings[setting].help);
    spinButton.set_tooltip_text(intSettings[setting].help);
  }

  hbox.pack_start(settingLabel, true, true, 0);
  hbox.add(spinButton);

  return hbox;
}

function _createColorSetting(setting) {
  let hbox = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL});

  let settingLabel = new Gtk.Label({label: colorSettings[setting].label,
                                    xalign: 0});

  let color = Lib.getColorByHexadecimal(settings.get_string(setting));
  let colorButton = new Gtk.ColorButton();
  colorButton.set_color(color);
  colorButton.connect('notify::color', function(button) {
    let hex = Lib.getHexadecimalByColor(button.get_color());
    settings.set_string(setting, hex);
  });

  if (colorSettings[setting].help) {
    settingLabel.set_tooltip_text(colorSettings[setting].help);
    colorButton.set_tooltip_text(colorSettings[setting].help);
  }

  hbox.pack_start(settingLabel, true, true, 0);
  hbox.add(colorButton);

  return hbox;
}

function _createBlacklistSetting() {
  let settingLabel = new Gtk.Label({label: _("Blacklist"), xalign: 0});
  let widget = new Widget();
  let blbox = new Gtk.Grid({column_spacing: 5, row_spacing: 5, margin: 0});
  blbox.attach(settingLabel,0,0,1,1);
  blbox.attach(widget,0,1,1,1);
  return blbox;
}

function _createFilterTypeSetting() {
  let hbox = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL});
  let settingLabel = new Gtk.Label({label: _("Filter Type"), xalign: 0});

  let listStore = new Gtk.ListStore();
  listStore.set_column_types ([
    GObject.TYPE_STRING,
    GObject.TYPE_STRING]);

  listStore.insert_with_valuesv (-1,  [0, 1], [0, "Blacklist"]);
  listStore.insert_with_valuesv (-1,  [0, 1], [1, "Whitelist"]);

  let filterComboBox = new Gtk.ComboBox({ model: listStore });
  filterComboBox.set_active (settings.get_int(SETTING_FILTER_TYPE));
  filterComboBox.set_id_column(0);

  let rendererText = new Gtk.CellRendererText();
  filterComboBox.pack_start (rendererText, false);
  filterComboBox.add_attribute (rendererText, "text", 1);

  filterComboBox.connect('changed', function(entry) {
    let id = filterComboBox.get_active_id();
    if (id == null)
        return;
    settings.set_int(SETTING_FILTER_TYPE, id);
  });

  hbox.pack_start(settingLabel, true, true, 0);
  hbox.add(filterComboBox);
  return hbox;
}

/*
   Shell-extensions handlers
*/

function init() {
  Lib.initTranslations(Me);
  settings = Lib.getSettings(Me);

  colorSettings = {
    color: {
      label: _("Alert color"),
      help: _("The color used to paint the message on user's menu")
    },
  };

  intSettings = {
    blinkrate: {
      label: _("Blink rate (in ms)"),
      help: _("The rate that the alert blinks, in ms. 0 means no blink (default: 800)"),
      min: 0,
      max: 10000,
      step: 1
    },
  };

  boolSettings = {
    chatonly: {
      label: _("Only alert for chat notifications"),
      help: _("Only chat notifications (like Empathy ones) will get alerted (default: OFF)")
    },
    force: {
      label: _("Force alerting even when notifications are set to OFF"),
      help: _("Alert even if you set notifications to OFF on user menu (default: OFF)")
    },
  };
}

/*
   Blacklist widget
*/
const Widget = new GObject.Class({
  Name: 'NotificationsAlert.Prefs.BlackListWidget',
  GTypeName: 'NotificationsAlertBlackListPrefsWidget',
  Extends: Gtk.Grid,

  _init: function(params) {
    this.parent(params);
    this.set_orientation(Gtk.Orientation.VERTICAL);

    Lib.initTranslations(Me);
    this._settings = Lib.getSettings(Me);

    this._store = new Gtk.ListStore();
    this._store.set_column_types([Gio.AppInfo, GObject.TYPE_STRING, Gio.Icon]);

    let scrolled = new Gtk.ScrolledWindow({ shadow_type: Gtk.ShadowType.IN});
    scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
    scrolled.set_min_content_height(150);
    this.add(scrolled);

    this._treeView = new Gtk.TreeView({ model: this._store,
                                        hexpand: true, vexpand: true });
    this._treeView.get_selection().set_mode(Gtk.SelectionMode.SINGLE);

    let appColumn = new Gtk.TreeViewColumn({ expand: true, sort_column_id: Columns.DISPLAY_NAME,
                                             title: _("Application") });
    let iconRenderer = new Gtk.CellRendererPixbuf;
    appColumn.pack_start(iconRenderer, false);
    appColumn.add_attribute(iconRenderer, "gicon", Columns.ICON);
    let nameRenderer = new Gtk.CellRendererText;
    appColumn.pack_start(nameRenderer, true);
    appColumn.add_attribute(nameRenderer, "text", Columns.DISPLAY_NAME);
    this._treeView.append_column(appColumn);

    scrolled.add(this._treeView);

    let toolbar = new Gtk.Toolbar({ icon_size: Gtk.IconSize.SMALL_TOOLBAR });
    toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR);
    this.add(toolbar);

    let newButton = new Gtk.ToolButton({ icon_name: 'bookmark-new-symbolic',
                                         label: _("Add Rule"),
                                         is_important: true });
    newButton.connect('clicked', Lang.bind(this, this._createNew));
    toolbar.add(newButton);

    let delButton = new Gtk.ToolButton({ icon_name: 'edit-delete-symbolic'  });
      delButton.connect('clicked', Lang.bind(this, this._deleteSelected));
      toolbar.add(delButton);

    this._changedPermitted = true;
    this._refresh();
  },

  _createNew: function() {
    let dialog = new Gtk.Dialog({ title: _("Blacklist app"),
                                  transient_for: this.get_toplevel(),
                                  use_header_bar: true,
                                  modal: true });
    dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL);
    let addButton = dialog.add_button(_("Add"), Gtk.ResponseType.OK);
    dialog.set_default_response(Gtk.ResponseType.OK);

    dialog._appChooser = new Gtk.AppChooserWidget({ show_all: true });

    let lbl = new Gtk.Label({label: _("Choose an application to blacklist:"),
                             xalign: 0.5});
    let hbox = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL,
                            margin: 5});
    hbox.pack_start(lbl, false, true, 0);
    hbox.pack_start(dialog._appChooser, true, true, 0);
    dialog.get_content_area().pack_start(hbox, true, true, 0);

    dialog.connect('response', Lang.bind(this, function(dialog, id) {
      if (id != Gtk.ResponseType.OK) {
        dialog.destroy();
        return;
      }

      let appInfo = dialog._appChooser.get_app_info();
      if (!appInfo) return;

      if (this._checkId( appInfo.get_id())){
        dialog.destroy();
        return;
      }

      this._changedPermitted = false;
      this._appendItem(appInfo.get_id());
      this._changedPermitted = true;

      let iter = this._store.append();

      this._store.set(iter,
        [Columns.APPINFO, Columns.ICON, Columns.DISPLAY_NAME],
        [appInfo, appInfo.get_icon(), appInfo.get_display_name()]);

      dialog.destroy();
      }));

    dialog.show_all();
  },

  _deleteSelected: function() {
    let [any, model, iter] = this._treeView.get_selection().get_selected();

    if (any) {
      let appInfo = this._store.get_value(iter, Columns.APPINFO);

      this._changedPermitted = false;
      this._removeItem(appInfo.get_id());
      this._changedPermitted = true;
      this._store.remove(iter);
    }
  },

  _refresh: function() {
    if (!this._changedPermitted)
        // Ignore this notification, model is being modified outside
        return;

    this._store.clear();

    let currentItems = this._settings.get_strv(SETTING_BLACKLIST);
    let validItems = [ ];
    for (let i = 0; i < currentItems.length; i++) {
      let id = currentItems[i];
      let appInfo = Gio.DesktopAppInfo.new(id);
      if (!appInfo)
        continue;
      validItems.push(currentItems[i]);

      let iter = this._store.append();
      this._store.set(iter,
          [Columns.APPINFO, Columns.ICON, Columns.DISPLAY_NAME],
          [appInfo, appInfo.get_icon(), appInfo.get_display_name()]);
    }

    if (validItems.length != currentItems.length) // some items were filtered out
        this._settings.set_strv(SETTING_BLACKLIST, validItems);
  },

  _checkId: function(id) {
    let items = this._settings.get_strv(SETTING_BLACKLIST);
    return (items.indexOf(id) != -1);
  },

  _appendItem: function(id) {
    let currentItems = this._settings.get_strv(SETTING_BLACKLIST);
    currentItems.push(id);
    this._settings.set_strv(SETTING_BLACKLIST, currentItems);
  },

  _removeItem: function(id) {
    let currentItems = this._settings.get_strv(SETTING_BLACKLIST);
    let index = currentItems.indexOf(id);

    if (index < 0)
      return;
    currentItems.splice(index, 1);
    this._settings.set_strv(SETTING_BLACKLIST, currentItems);
  }
});

function buildPrefsWidget() {
  let frame = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL,
                           border_width: 10});
  let vbox = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL,
                          margin: 20, margin_top: 10, spacing: 5});
  let setting;

  // Add all color settings
  for (setting in colorSettings) {
    let hbox = _createColorSetting(setting);
    vbox.add(hbox);
  }
  // Add all bool settings
  for (setting in boolSettings) {
    let hbox = _createBoolSetting(setting);
    vbox.add(hbox);
  }
  // Add all int settings
  for (setting in intSettings) {
    let hbox = _createIntSetting(setting);
    vbox.add(hbox);
  }

  // Add filter type setting
  let filterType = _createFilterTypeSetting();
  vbox.add(filterType);

  // Add filter list
  let blbox = _createBlacklistSetting();
  vbox.add(blbox);

  frame.add(vbox);
  frame.show_all();

  return frame;
}
<commit_msg>small improvements to filter gui (#28)<commit_after>/*
 * Copyright (C) 2012 Thiago Bellini <hackedbellini@gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Most of this code was forked from media-player-indicator:
 *   https://extensions.gnome.org/extension/55/media-player-indicator/
 *
 */

const GObject = imports.gi.GObject;
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;

const Gettext = imports.gettext.domain('gnome-shell-notifications-alert');
const _ = Gettext.gettext;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Lib = Me.imports.lib;

const SETTING_BLACKLIST = 'application-list';
const SETTING_FILTER_TYPE = 'filter';

const Columns = {
  APPINFO: 0,
  DISPLAY_NAME: 1,
  ICON: 2,
};

let settings;
let boolSettings;
let intSettings;
let colorSettings;

function _createBoolSetting(setting) {
  let hbox = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL});

  let settingLabel = new Gtk.Label({label: boolSettings[setting].label,
                                    xalign: 0});

  let settingSwitch = new Gtk.Switch({active: settings.get_boolean(setting)});
  settingSwitch.connect('notify::active', function(button) {
    settings.set_boolean(setting, button.active);
  });

  if (boolSettings[setting].help) {
    settingLabel.set_tooltip_text(boolSettings[setting].help);
    settingSwitch.set_tooltip_text(boolSettings[setting].help);
  }

  hbox.pack_start(settingLabel, true, true, 0);
  hbox.add(settingSwitch);

  return hbox;
}

function _createIntSetting(setting) {
  let hbox = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL});

  let settingLabel = new Gtk.Label({label: intSettings[setting].label,
                                    xalign: 0});

  let spinButton = Gtk.SpinButton.new_with_range(
    intSettings[setting].min,
    intSettings[setting].max,
    intSettings[setting].step)
  spinButton.set_value(settings.get_int(setting));
  spinButton.connect('notify::value', function(spin) {
    settings.set_int(setting, spin.get_value_as_int());
  });

  if (intSettings[setting].help) {
    settingLabel.set_tooltip_text(intSettings[setting].help);
    spinButton.set_tooltip_text(intSettings[setting].help);
  }

  hbox.pack_start(settingLabel, true, true, 0);
  hbox.add(spinButton);

  return hbox;
}

function _createColorSetting(setting) {
  let hbox = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL});

  let settingLabel = new Gtk.Label({label: colorSettings[setting].label,
                                    xalign: 0});

  let color = Lib.getColorByHexadecimal(settings.get_string(setting));
  let colorButton = new Gtk.ColorButton();
  colorButton.set_color(color);
  colorButton.connect('notify::color', function(button) {
    let hex = Lib.getHexadecimalByColor(button.get_color());
    settings.set_string(setting, hex);
  });

  if (colorSettings[setting].help) {
    settingLabel.set_tooltip_text(colorSettings[setting].help);
    colorButton.set_tooltip_text(colorSettings[setting].help);
  }

  hbox.pack_start(settingLabel, true, true, 0);
  hbox.add(colorButton);

  return hbox;
}

function _createFilterListSetting() {
  let settingLabel = new Gtk.Label({label: _("Filter List"), xalign: 0});
  let widget = new Widget();
  let blbox = new Gtk.Grid({column_spacing: 5, row_spacing: 5, margin: 0});
  blbox.attach(settingLabel,0,0,1,1);
  blbox.attach(widget,0,1,1,1);
  return blbox;
}

function _createFilterTypeSetting() {
  let hbox = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL});
  let settingLabel = new Gtk.Label({label: _("Filter Type"), xalign: 0});

  let listStore = new Gtk.ListStore();
  listStore.set_column_types ([
    GObject.TYPE_STRING,
    GObject.TYPE_STRING]);

  listStore.insert_with_valuesv (-1,  [0, 1], [0, "Blacklist"]);
  listStore.insert_with_valuesv (-1,  [0, 1], [1, "Whitelist"]);

  let filterComboBox = new Gtk.ComboBox({ model: listStore });
  filterComboBox.set_active (settings.get_int(SETTING_FILTER_TYPE));
  filterComboBox.set_id_column(0);

  let rendererText = new Gtk.CellRendererText();
  filterComboBox.pack_start (rendererText, false);
  filterComboBox.add_attribute (rendererText, "text", 1);

  filterComboBox.connect('changed', function(entry) {
    let id = filterComboBox.get_active_id();
    if (id == null)
        return;
    settings.set_int(SETTING_FILTER_TYPE, id);
  });

  hbox.pack_start(settingLabel, true, true, 0);
  hbox.add(filterComboBox);
  return hbox;
}

/*
   Shell-extensions handlers
*/

function init() {
  Lib.initTranslations(Me);
  settings = Lib.getSettings(Me);

  colorSettings = {
    color: {
      label: _("Alert color"),
      help: _("The color used to paint the message on user's menu")
    },
  };

  intSettings = {
    blinkrate: {
      label: _("Blink rate (in ms)"),
      help: _("The rate that the alert blinks, in ms. 0 means no blink (default: 800)"),
      min: 0,
      max: 10000,
      step: 1
    },
  };

  boolSettings = {
    chatonly: {
      label: _("Only alert for chat notifications"),
      help: _("Only chat notifications (like Empathy ones) will get alerted (default: OFF)")
    },
    force: {
      label: _("Force alerting even when notifications are set to OFF"),
      help: _("Alert even if you set notifications to OFF on user menu (default: OFF)")
    },
  };
}

/*
   Blacklist widget
*/
const Widget = new GObject.Class({
  Name: 'NotificationsAlert.Prefs.BlackListWidget',
  GTypeName: 'NotificationsAlertBlackListPrefsWidget',
  Extends: Gtk.Grid,

  _init: function(params) {
    this.parent(params);
    this.set_orientation(Gtk.Orientation.VERTICAL);

    Lib.initTranslations(Me);
    this._settings = Lib.getSettings(Me);

    this._store = new Gtk.ListStore();
    this._store.set_column_types([Gio.AppInfo, GObject.TYPE_STRING, Gio.Icon]);

    let scrolled = new Gtk.ScrolledWindow({ shadow_type: Gtk.ShadowType.IN});
    scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
    scrolled.set_min_content_height(150);
    this.add(scrolled);

    this._treeView = new Gtk.TreeView({ model: this._store,
                                        hexpand: true, vexpand: true });
    this._treeView.get_selection().set_mode(Gtk.SelectionMode.SINGLE);

    let appColumn = new Gtk.TreeViewColumn({ expand: true, sort_column_id: Columns.DISPLAY_NAME,
                                             title: _("Application") });
    let iconRenderer = new Gtk.CellRendererPixbuf;
    appColumn.pack_start(iconRenderer, false);
    appColumn.add_attribute(iconRenderer, "gicon", Columns.ICON);
    let nameRenderer = new Gtk.CellRendererText;
    appColumn.pack_start(nameRenderer, true);
    appColumn.add_attribute(nameRenderer, "text", Columns.DISPLAY_NAME);
    this._treeView.append_column(appColumn);

    scrolled.add(this._treeView);

    let toolbar = new Gtk.Toolbar({ icon_size: Gtk.IconSize.SMALL_TOOLBAR });
    toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR);
    this.add(toolbar);

    let newButton = new Gtk.ToolButton({ icon_name: 'bookmark-new-symbolic',
                                         label: _("Add Rule"),
                                         is_important: true });
    newButton.connect('clicked', Lang.bind(this, this._createNew));
    toolbar.add(newButton);

    let delButton = new Gtk.ToolButton({ icon_name: 'edit-delete-symbolic'  });
      delButton.connect('clicked', Lang.bind(this, this._deleteSelected));
      toolbar.add(delButton);

    this._changedPermitted = true;
    this._refresh();
  },

  _createNew: function() {
    let dialog = new Gtk.Dialog({ title: _("Blacklist app"),
                                  transient_for: this.get_toplevel(),
                                  use_header_bar: true,
                                  modal: true });
    dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL);
    let addButton = dialog.add_button(_("Add"), Gtk.ResponseType.OK);
    dialog.set_default_response(Gtk.ResponseType.OK);

    dialog._appChooser = new Gtk.AppChooserWidget({ show_all: true });

    let lbl = new Gtk.Label({label: _("Choose an application to blacklist:"),
                             xalign: 0.5});
    let hbox = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL,
                            margin: 5});
    hbox.pack_start(lbl, false, true, 0);
    hbox.pack_start(dialog._appChooser, true, true, 0);
    dialog.get_content_area().pack_start(hbox, true, true, 0);

    dialog.connect('response', Lang.bind(this, function(dialog, id) {
      if (id != Gtk.ResponseType.OK) {
        dialog.destroy();
        return;
      }

      let appInfo = dialog._appChooser.get_app_info();
      if (!appInfo) return;

      if (this._checkId( appInfo.get_id())){
        dialog.destroy();
        return;
      }

      this._changedPermitted = false;
      this._appendItem(appInfo.get_id());
      this._changedPermitted = true;

      let iter = this._store.append();

      this._store.set(iter,
        [Columns.APPINFO, Columns.ICON, Columns.DISPLAY_NAME],
        [appInfo, appInfo.get_icon(), appInfo.get_display_name()]);

      dialog.destroy();
      }));

    dialog.show_all();
  },

  _deleteSelected: function() {
    let [any, model, iter] = this._treeView.get_selection().get_selected();

    if (any) {
      let appInfo = this._store.get_value(iter, Columns.APPINFO);

      this._changedPermitted = false;
      this._removeItem(appInfo.get_id());
      this._changedPermitted = true;
      this._store.remove(iter);
    }
  },

  _refresh: function() {
    if (!this._changedPermitted)
        // Ignore this notification, model is being modified outside
        return;

    this._store.clear();

    let currentItems = this._settings.get_strv(SETTING_BLACKLIST);
    let validItems = [ ];
    for (let i = 0; i < currentItems.length; i++) {
      let id = currentItems[i];
      let appInfo = Gio.DesktopAppInfo.new(id);
      if (!appInfo)
        continue;
      validItems.push(currentItems[i]);

      let iter = this._store.append();
      this._store.set(iter,
          [Columns.APPINFO, Columns.ICON, Columns.DISPLAY_NAME],
          [appInfo, appInfo.get_icon(), appInfo.get_display_name()]);
    }

    if (validItems.length != currentItems.length) // some items were filtered out
        this._settings.set_strv(SETTING_BLACKLIST, validItems);
  },

  _checkId: function(id) {
    let items = this._settings.get_strv(SETTING_BLACKLIST);
    return (items.indexOf(id) != -1);
  },

  _appendItem: function(id) {
    let currentItems = this._settings.get_strv(SETTING_BLACKLIST);
    currentItems.push(id);
    this._settings.set_strv(SETTING_BLACKLIST, currentItems);
  },

  _removeItem: function(id) {
    let currentItems = this._settings.get_strv(SETTING_BLACKLIST);
    let index = currentItems.indexOf(id);

    if (index < 0)
      return;
    currentItems.splice(index, 1);
    this._settings.set_strv(SETTING_BLACKLIST, currentItems);
  }
});

function buildPrefsWidget() {
  let frame = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL,
                           border_width: 10});
  let vbox = new Gtk.Box({orientation: Gtk.Orientation.VERTICAL,
                          margin: 20, margin_top: 10, spacing: 5});
  let setting;

  // Add all color settings
  for (setting in colorSettings) {
    let hbox = _createColorSetting(setting);
    vbox.add(hbox);
  }
  // Add all bool settings
  for (setting in boolSettings) {
    let hbox = _createBoolSetting(setting);
    vbox.add(hbox);
  }
  // Add all int settings
  for (setting in intSettings) {
    let hbox = _createIntSetting(setting);
    vbox.add(hbox);
  }

  // Add filter type setting
  let filterType = _createFilterTypeSetting();
  vbox.add(filterType);

  // Add filter list
  let blbox = _createFilterListSetting();
  vbox.add(blbox);

  frame.add(vbox);
  frame.show_all();

  return frame;
}
<|endoftext|>


<commit_before>import alignProps from 'vue-bulma-common/src/mixins/align.js'

export default {
  name: 'Vb-buttons-list',
  mixins: [alignProps],
  props: {
    addons: Boolean
  },
  computed: {
    classes() {
      const { align, addons } = this
      return {
        buttons: true,
        [`is-${align}`]: !!align,
        'has-addons': addons
      }
    }
  },
  render(h) {
    return h('div', { class: classes }, $slots.default)
  }
}
<commit_msg>fixup: classes is undefined<commit_after>import alignProps from 'vue-bulma-common/src/mixins/align.js'

export default {
  name: 'vb-buttons-list',
  mixins: [alignProps],
  props: {
    addons: Boolean
  },
  computed: {
    classes() {
      const { align, addons } = this
      return {
        buttons: true,
        [`is-${align}`]: !!align,
        'has-addons': addons
      }
    }
  },
  render(h) {
    const { classes, $slots } = this
    return h('div', { class: classes }, $slots.default)
  }
}
<|endoftext|>


<commit_before>import { Utils } from 'meteor/vulcan:lib';
import Users from './collection.js';
import moment from 'moment';
import _ from 'underscore';

////////////////////
//  User Getters  //
////////////////////

/**
 * @summary Get a user
 * @param {String} userOrUserId
 */
Users.getUser = function(userOrUserId) {
  if (typeof userOrUserId === 'undefined') {
    if (!Meteor.user()) {
      throw new Error();
    } else {
      return Meteor.user();
    }
  } else if (typeof userOrUserId === 'string') {
    return Users.findOne(userOrUserId);
  } else {
    return userOrUserId;
  }
};

/**
 * @summary Get a user's username (unique, no special characters or spaces)
 * @param {Object} user
 */
Users.getUserName = function(user) {
  try {
    if (user && user.username) return user.username; //Add null check for user
    if (user && user.services && user.services.twitter && user.services.twitter.screenName)
      return user.services.twitter.screenName;
  } catch (error) {
    console.log(error); // eslint-disable-line
    return null;
  }
};
Users.getUserNameById = function(userId) {
  return Users.getUserName(Users.findOne(userId));
};

/**
 * @summary Get a user's display name (not unique, can take special characters and spaces)
 * @param {Object} user
 */
Users.getDisplayName = function(user) {
  if (!user) {
    return '';
  } else {
    return user.displayName ? user.displayName : Users.getUserName(user);
  }
};
Users.getDisplayNameById = function(userId) {
  return Users.getDisplayName(Users.findOne(userId));
};
export const getDisplayName = Users.getDisplayName;

/**
 * @summary Get a user's profile URL
 * @param {Object} user (note: we only actually need either the _id or slug properties)
 * @param {Boolean} isAbsolute
 */
Users.getProfileUrl = function(user, isAbsolute) {
  if (typeof user === 'undefined') {
    return '';
  }
  isAbsolute = typeof isAbsolute === 'undefined' ? false : isAbsolute; // default to false
  var prefix = isAbsolute ? Utils.getSiteUrl().slice(0, -1) : '';
  if (user.slug) {
    return `${prefix}/users/${user.slug}`;
  } else {
    return '';
  }
};
export const getProfileUrl = Users.getProfileUrl;

/**
 * @summary Get a user's account edit URL
 * @param {Object} user (note: we only actually need either the _id or slug properties)
 * @param {Boolean} isAbsolute
 */
Users.getEditUrl = function(user, isAbsolute) {
  return `${Users.getProfileUrl(user, isAbsolute)}/edit`;
};

/**
 * @summary Get a user's Twitter name
 * @param {Object} user
 */
Users.getTwitterName = function(user) {
  // return twitter name provided by user, or else the one used for twitter login
  if (typeof user !== 'undefined') {
    if (user.twitterUsername) {
      return user.twitterUsername;
    } else if (Utils.checkNested(user, 'services', 'twitter', 'screenName')) {
      return user.services.twitter.screenName;
    }
  }
  return null;
};
Users.getTwitterNameById = function(userId) {
  return Users.getTwitterName(Users.findOne(userId));
};

/**
 * @summary Get a user's GitHub name
 * @param {Object} user
 */
Users.getGitHubName = function(user) {
  // return twitter name provided by user, or else the one used for twitter login
  if (Utils.checkNested(user, 'profile', 'github')) {
    return user.profile.github;
  } else if (Utils.checkNested(user, 'services', 'github', 'screenName')) {
    // TODO: double-check this with GitHub login
    return user.services.github.screenName;
  }
  return null;
};
Users.getGitHubNameById = function(userId) {
  return Users.getGitHubName(Users.findOne(userId));
};

/**
 * @summary Get a user's email
 * @param {Object} user
 */
Users.getEmail = function(user) {
  if (user.email) {
    return user.email;
  } else {
    return null;
  }
};
Users.getEmailById = function(userId) {
  return Users.getEmail(Users.findOne(userId));
};

/**
 * @summary Get a user's email hash
 * @param {Object} user
 */
Users.getEmailHash = function(user) {
  return user.emailHash;
};
Users.getEmailHashById = function(userId) {
  return Users.getEmailHash(Users.findOne(userId));
};

/**
 * @summary Get a user setting
 * @param {Object} user
 * @param {String} settingName
 * @param {Object} defaultValue
 */
Users.getSetting = function(user = null, settingName, defaultValue = null) {
  if (user) {
    const settingValue = Users.getProperty(user, settingName);
    return typeof settingValue === 'undefined' ? defaultValue : settingValue;
  } else {
    return defaultValue;
  }
};

////////////////////
//  User Checks   //
////////////////////

/**
 * @summary Check if the user has completed their profile.
 * @param {Object} user
 */
Users.hasCompletedProfile = function(user) {
  if (!user) return false;

  return _.every(Users.getRequiredFields(), function(fieldName) {
    return !!Utils.getNestedProperty(user, fieldName);
  });
};

///////////////////
// Other Helpers //
///////////////////

Users.findLast = function(user, collection) {
  return collection.findOne({ userId: user._id }, { sort: { createdAt: -1 } });
};

Users.timeSinceLast = function(user, collection) {
  var now = new Date().getTime();
  var last = this.findLast(user, collection);
  if (!last) return 999; // if this is the user's first post or comment ever, stop here
  return Math.abs(Math.floor((now - last.createdAt) / 1000));
};

Users.numberOfItemsInPast24Hours = function(user, collection) {
  var mNow = moment();
  var items = collection.find({
    userId: user._id,
    createdAt: {
      $gte: mNow.subtract(24, 'hours').toDate(),
    },
  });
  return items.count();
};

Users.getProperty = function(object, property) {
  // recursive function to get nested properties
  var array = property.split('.');
  if (array.length > 1) {
    var parent = array.shift();
    // if our property is not at this level, call function again one level deeper if we can go deeper, else return undefined
    return typeof object[parent] === 'undefined'
      ? undefined
      : this.getProperty(object[parent], array.join('.'));
  } else {
    // else return property
    return object[array[0]];
  }
};

// Users.setSetting = (user, settingName, value) => {
//   // all users settings should begin with the prexi : user.setting namespace, so add "" if needed
//   var field = settingName.slice(0,2) === "" ? settingName : "" + settingName;

//   var modifier = {$set: {}};
//   modifier.$set[field] = value;

//   Users.update(user._id, modifier);
// }

////////////////////
// More Helpers   //
////////////////////

// helpers that don't take a user as argument

/**
 * @summary @method Users.getRequiredFields
 * Get a list of all fields required for a profile to be complete.
 */
Users.getRequiredFields = function() {
  var schema = Users.simpleSchema()._schema;
  var fields = _.filter(_.keys(schema), function(fieldName) {
    var field = schema[fieldName];
    return !!field.mustComplete;
  });
  return fields;
};

// Users.adminUsers = function (options) {
//   return this.find({isAdmin : true}, options).fetch();
// };

// Users.getCurrentUserEmail = function () {
//   return Meteor.user() ? Users.getEmail(Meteor.user()) : '';
// };

Users.findByEmail = function(email) {
  return Users.findOne({ email: email });
};
<commit_msg>Update helpers.js<commit_after>import { Utils } from 'meteor/vulcan:lib';
import Users from './collection.js';
import moment from 'moment';
import _ from 'underscore';

////////////////////
//  User Getters  //
////////////////////

/**
 * @summary Get a user
 * @param {String} userOrUserId
 */
Users.getUser = function(userOrUserId) {
  if (typeof userOrUserId === 'undefined') {
    if (!Meteor.user()) {
      throw new Error();
    } else {
      return Meteor.user();
    }
  } else if (typeof userOrUserId === 'string') {
    return Users.findOne(userOrUserId);
  } else {
    return userOrUserId;
  }
};

/**
 * @summary Get a user's username (unique, no special characters or spaces)
 * @param {Object} user
 */
Users.getUserName = function(user) {
  try {
    if (user.username) return user.username;
    if (user && user.services && user.services.twitter && user.services.twitter.screenName)
      return user.services.twitter.screenName;
  } catch (error) {
    console.log(error); // eslint-disable-line
    return null;
  }
};
Users.getUserNameById = function(userId) {
  return Users.getUserName(Users.findOne(userId));
};

/**
 * @summary Get a user's display name (not unique, can take special characters and spaces)
 * @param {Object} user
 */
Users.getDisplayName = function(user) {
  if (!user) {
    return '';
  } else {
    return user.displayName ? user.displayName : Users.getUserName(user);
  }
};
Users.getDisplayNameById = function(userId) {
  return Users.getDisplayName(Users.findOne(userId));
};
export const getDisplayName = Users.getDisplayName;

/**
 * @summary Get a user's profile URL
 * @param {Object} user (note: we only actually need either the _id or slug properties)
 * @param {Boolean} isAbsolute
 */
Users.getProfileUrl = function(user, isAbsolute) {
  if (typeof user === 'undefined') {
    return '';
  }
  isAbsolute = typeof isAbsolute === 'undefined' ? false : isAbsolute; // default to false
  var prefix = isAbsolute ? Utils.getSiteUrl().slice(0, -1) : '';
  if (user.slug) {
    return `${prefix}/users/${user.slug}`;
  } else {
    return '';
  }
};
export const getProfileUrl = Users.getProfileUrl;

/**
 * @summary Get a user's account edit URL
 * @param {Object} user (note: we only actually need either the _id or slug properties)
 * @param {Boolean} isAbsolute
 */
Users.getEditUrl = function(user, isAbsolute) {
  return `${Users.getProfileUrl(user, isAbsolute)}/edit`;
};

/**
 * @summary Get a user's Twitter name
 * @param {Object} user
 */
Users.getTwitterName = function(user) {
  // return twitter name provided by user, or else the one used for twitter login
  if (typeof user !== 'undefined') {
    if (user.twitterUsername) {
      return user.twitterUsername;
    } else if (Utils.checkNested(user, 'services', 'twitter', 'screenName')) {
      return user.services.twitter.screenName;
    }
  }
  return null;
};
Users.getTwitterNameById = function(userId) {
  return Users.getTwitterName(Users.findOne(userId));
};

/**
 * @summary Get a user's GitHub name
 * @param {Object} user
 */
Users.getGitHubName = function(user) {
  // return twitter name provided by user, or else the one used for twitter login
  if (Utils.checkNested(user, 'profile', 'github')) {
    return user.profile.github;
  } else if (Utils.checkNested(user, 'services', 'github', 'screenName')) {
    // TODO: double-check this with GitHub login
    return user.services.github.screenName;
  }
  return null;
};
Users.getGitHubNameById = function(userId) {
  return Users.getGitHubName(Users.findOne(userId));
};

/**
 * @summary Get a user's email
 * @param {Object} user
 */
Users.getEmail = function(user) {
  if (user.email) {
    return user.email;
  } else {
    return null;
  }
};
Users.getEmailById = function(userId) {
  return Users.getEmail(Users.findOne(userId));
};

/**
 * @summary Get a user's email hash
 * @param {Object} user
 */
Users.getEmailHash = function(user) {
  return user.emailHash;
};
Users.getEmailHashById = function(userId) {
  return Users.getEmailHash(Users.findOne(userId));
};

/**
 * @summary Get a user setting
 * @param {Object} user
 * @param {String} settingName
 * @param {Object} defaultValue
 */
Users.getSetting = function(user = null, settingName, defaultValue = null) {
  if (user) {
    const settingValue = Users.getProperty(user, settingName);
    return typeof settingValue === 'undefined' ? defaultValue : settingValue;
  } else {
    return defaultValue;
  }
};

////////////////////
//  User Checks   //
////////////////////

/**
 * @summary Check if the user has completed their profile.
 * @param {Object} user
 */
Users.hasCompletedProfile = function(user) {
  if (!user) return false;

  return _.every(Users.getRequiredFields(), function(fieldName) {
    return !!Utils.getNestedProperty(user, fieldName);
  });
};

///////////////////
// Other Helpers //
///////////////////

Users.findLast = function(user, collection) {
  return collection.findOne({ userId: user._id }, { sort: { createdAt: -1 } });
};

Users.timeSinceLast = function(user, collection) {
  var now = new Date().getTime();
  var last = this.findLast(user, collection);
  if (!last) return 999; // if this is the user's first post or comment ever, stop here
  return Math.abs(Math.floor((now - last.createdAt) / 1000));
};

Users.numberOfItemsInPast24Hours = function(user, collection) {
  var mNow = moment();
  var items = collection.find({
    userId: user._id,
    createdAt: {
      $gte: mNow.subtract(24, 'hours').toDate(),
    },
  });
  return items.count();
};

Users.getProperty = function(object, property) {
  // recursive function to get nested properties
  var array = property.split('.');
  if (array.length > 1) {
    var parent = array.shift();
    // if our property is not at this level, call function again one level deeper if we can go deeper, else return undefined
    return typeof object[parent] === 'undefined'
      ? undefined
      : this.getProperty(object[parent], array.join('.'));
  } else {
    // else return property
    return object[array[0]];
  }
};

// Users.setSetting = (user, settingName, value) => {
//   // all users settings should begin with the prexi : user.setting namespace, so add "" if needed
//   var field = settingName.slice(0,2) === "" ? settingName : "" + settingName;

//   var modifier = {$set: {}};
//   modifier.$set[field] = value;

//   Users.update(user._id, modifier);
// }

////////////////////
// More Helpers   //
////////////////////

// helpers that don't take a user as argument

/**
 * @summary @method Users.getRequiredFields
 * Get a list of all fields required for a profile to be complete.
 */
Users.getRequiredFields = function() {
  var schema = Users.simpleSchema()._schema;
  var fields = _.filter(_.keys(schema), function(fieldName) {
    var field = schema[fieldName];
    return !!field.mustComplete;
  });
  return fields;
};

// Users.adminUsers = function (options) {
//   return this.find({isAdmin : true}, options).fetch();
// };

// Users.getCurrentUserEmail = function () {
//   return Meteor.user() ? Users.getEmail(Meteor.user()) : '';
// };

Users.findByEmail = function(email) {
  return Users.findOne({ email: email });
};
<|endoftext|>


<commit_before>import React from 'react'
import { Link } from 'react-router'
import { filter, find, isEmpty } from 'lodash'
const { array, bool, func, object } = React.PropTypes
import cx from 'classnames'
import { humanDate, present, sanitize, timeRange, timeRangeFull } from '../util/text'
import truncate from 'html-truncate'
import A from './A'
import Avatar from './Avatar'
import Dropdown from './Dropdown'
import ClickCatchingDiv from './ClickCatchingDiv'
import Comment from './Comment'
import CommentForm from './CommentForm'
import PostEditor from './PostEditor'
import RSVPControl from './RSVPControl'
import { connect } from 'react-redux'
import { fetchComments, createComment, startPostEdit, changeEventResponse } from '../actions'

const spacer =   •

@connect(({ comments, commentsByPost, people, postEdits, communities }, { post }) => ({
  comments: (commentsByPost[post.id] || []).map(id => comments[id]),
  currentUser: people.current,
  editing: !!postEdits[post.id],
  communities: post.communities.map(id => find(communities, c => c.id === id))
}))
export default class Post extends React.Component {
  static propTypes = {
    post: object,
    onExpand: func,
    expanded: bool,
    commentsExpanded: bool,
    communities: array,
    comments: array,
    dispatch: func,
    commentingDisabled: bool,
    currentUser: object,
    editing: bool
  }

  constructor (props) {
    super(props)
    this.state = {commentsExpanded: props.commentsExpanded}
  }

  expand = () => {
    let {expanded, onExpand, post} = this.props
    if (!expanded) onExpand(post.id)
  }

  toggleComments = event => {
    event.stopPropagation()
    event.preventDefault()
    this.expand()

    let { post, comments } = this.props
    if (!comments) this.props.dispatch(fetchComments(post.id))
    this.setState({commentsExpanded: !this.state.commentsExpanded})
  }

  edit = () => {
    let { dispatch, post } = this.props
    dispatch(startPostEdit(post))
  }

  render () {
    let { post, expanded, currentUser, editing } = this.props
    if (editing) return this.renderEdit()
    if (post.type === 'welcome') return this.renderWelcome()

    let image = find(post.media, m => m.type === 'image')
    var style = image ? {backgroundImage: `url(${image.url})`} : {}
    var classes = cx('post', post.type, {expanded: expanded, image: !!image})

    let title = post.name
    let person = post.user

    let isEvent = post.type === 'event'
    if (isEvent) {
      let start = new Date(post.start_time)
      let end = post.end_time && new Date(post.end_time)
      var eventTime = timeRange(start, end)
      var eventTimeFull = timeRangeFull(start, end)
    }

    let canEdit = (currentUser && currentUser.id === person.id)

    return

        <Avatar person={person}/>

        {expanded && <Dropdown className='post-menu' alignRight={true} toggleChildren={

          }>
            {canEdit &&
Edit
}

               window.alert('TODO')}>Report objectionable content

          </Dropdown>}

        {person.name}
        <PostMeta post={post} toggleComments={this.toggleComments}/>

{title}

      {isEvent &&

        {eventTime}

}

      {post.location &&

        {post.location}

}

      {image &&

}
      {expanded && <ExpandedPostDetails
        onCommentCreate={text => this.props.dispatch(createComment(post.id, text))}
        commentsExpanded={this.state.commentsExpanded}
        {...{image}} {...this.props}/>}

  }

  renderWelcome () {
    let { post, communities, expanded, dispatch } = this.props
    let person = post.relatedUsers[0]
    return

      <Avatar person={person}/>

        <A to={`/u/${person.id}`}>{person.name} joined {communities[0].name}.
        Welcome them!
        <PostMeta post={post} toggleComments={this.toggleComments}/>

      {expanded && <ExpandedPostDetails
        onCommentCreate={text => dispatch(createComment(post.id, text))}
        commentsExpanded={this.state.commentsExpanded}
        {...this.props}/>}

  }

  renderEdit () {
    let { post } = this.props
    return <PostEditor post={post} expanded={true}/>
  }
}

const PostMeta = ({ post, toggleComments }) => {
  const now = new Date()
  const createdAt = new Date(post.created_at)
  const updatedAt = new Date(post.updated_at)
  const shouldShowUpdatedAt = (now - updatedAt) < (now - createdAt) * 0.8

  return

    <A to={`/p/${post.id}`}>{humanDate(createdAt).replace(/ /g, String.fromCharCode(160))}
    {shouldShowUpdatedAt &&
      {spacer}updated {humanDate(updatedAt).replace(/ /g, String.fromCharCode(160))}
    }
    {spacer}
    {post.votes} ♡
    {spacer}

      {post.numComments} comment{post.numComments === 1 ? '' : 's'}

    {post.public &&
      {spacer}Public
    }

}

const ExpandedPostDetails = props => {
  let {
    post, image, comments, commentsExpanded, currentUser, dispatch,
    commentingDisabled, onCommentCreate, communities
  } = props
  let description = present(sanitize(post.description))
  let attachments = filter(post.media, m => m.type !== 'image')
  if (!comments) comments = []

  return

    {image && }

    {description && <ClickCatchingDiv className='details post-section'
      dangerouslySetInnerHTML={{__html: description}}/>}

    {post.type === 'event' && <RSVPControl
      responders={post.responders}
      currentResponse={(find(post.responders, responder => responder.id === currentUser.id) || {response: ''}).response}
      onPickResponse={choice => dispatch(changeEventResponse(post.id, choice, currentUser))} />}

    {!isEmpty(attachments) &&

      {attachments.map((file, i) =>

          {truncate(file.name, 40)}
        )}

}

    {post.public &&

      <Dropdown
        toggleChildren={
Share
}>

Facebook

Twitter

      </Dropdown>

}

{post.type}

        {communities.map(c =>

          <Link to={`/c/${c.slug}`} key={c.id}>{c.name}</Link>

)}

    {commentsExpanded &&

      {comments.map(c => <Comment comment={c} key={c.id}/>)}
      {!commentingDisabled && <CommentForm onCreate={onCommentCreate}/>}

}

}
<commit_msg>add facebook sharing<commit_after>import React from 'react'
import { Link } from 'react-router'
import { filter, find, isEmpty } from 'lodash'
const { array, bool, func, object } = React.PropTypes
import cx from 'classnames'
import { humanDate, present, sanitize, timeRange, timeRangeFull } from '../util/text'
import truncate from 'html-truncate'
import A from './A'
import Avatar from './Avatar'
import Dropdown from './Dropdown'
import ClickCatchingDiv from './ClickCatchingDiv'
import Comment from './Comment'
import CommentForm from './CommentForm'
import PostEditor from './PostEditor'
import RSVPControl from './RSVPControl'
import { connect } from 'react-redux'
import { fetchComments, createComment, startPostEdit, changeEventResponse } from '../actions'

const spacer =   •

@connect(({ comments, commentsByPost, people, postEdits, communities }, { post }) => ({
  comments: (commentsByPost[post.id] || []).map(id => comments[id]),
  currentUser: people.current,
  editing: !!postEdits[post.id],
  communities: post.communities.map(id => find(communities, c => c.id === id))
}))
export default class Post extends React.Component {
  static propTypes = {
    post: object,
    onExpand: func,
    expanded: bool,
    commentsExpanded: bool,
    communities: array,
    comments: array,
    dispatch: func,
    commentingDisabled: bool,
    currentUser: object,
    editing: bool
  }

  constructor (props) {
    super(props)
    this.state = {commentsExpanded: props.commentsExpanded}
  }

  expand = () => {
    let {expanded, onExpand, post} = this.props
    if (!expanded) onExpand(post.id)
  }

  toggleComments = event => {
    event.stopPropagation()
    event.preventDefault()
    this.expand()

    let { post, comments } = this.props
    if (!comments) this.props.dispatch(fetchComments(post.id))
    this.setState({commentsExpanded: !this.state.commentsExpanded})
  }

  edit = () => {
    let { dispatch, post } = this.props
    dispatch(startPostEdit(post))
  }

  render () {
    let { post, expanded, currentUser, editing } = this.props
    if (editing) return this.renderEdit()
    if (post.type === 'welcome') return this.renderWelcome()

    let image = find(post.media, m => m.type === 'image')
    var style = image ? {backgroundImage: `url(${image.url})`} : {}
    var classes = cx('post', post.type, {expanded: expanded, image: !!image})

    let title = post.name
    let person = post.user

    let isEvent = post.type === 'event'
    if (isEvent) {
      let start = new Date(post.start_time)
      let end = post.end_time && new Date(post.end_time)
      var eventTime = timeRange(start, end)
      var eventTimeFull = timeRangeFull(start, end)
    }

    let canEdit = (currentUser && currentUser.id === person.id)

    return

        <Avatar person={person}/>

        {expanded && <Dropdown className='post-menu' alignRight={true} toggleChildren={

          }>
            {canEdit &&
Edit
}

               window.alert('TODO')}>Report objectionable content

          </Dropdown>}

        {person.name}
        <PostMeta post={post} toggleComments={this.toggleComments}/>

{title}

      {isEvent &&

        {eventTime}

}

      {post.location &&

        {post.location}

}

      {image &&

}
      {expanded && <ExpandedPostDetails
        onCommentCreate={text => this.props.dispatch(createComment(post.id, text))}
        commentsExpanded={this.state.commentsExpanded}
        {...{image}} {...this.props}/>}

  }

  renderWelcome () {
    let { post, communities, expanded, dispatch } = this.props
    let person = post.relatedUsers[0]
    return

      <Avatar person={person}/>

        <A to={`/u/${person.id}`}>{person.name} joined {communities[0].name}.
        Welcome them!
        <PostMeta post={post} toggleComments={this.toggleComments}/>

      {expanded && <ExpandedPostDetails
        onCommentCreate={text => dispatch(createComment(post.id, text))}
        commentsExpanded={this.state.commentsExpanded}
        {...this.props}/>}

  }

  renderEdit () {
    let { post } = this.props
    return <PostEditor post={post} expanded={true}/>
  }
}

const PostMeta = ({ post, toggleComments }) => {
  const now = new Date()
  const createdAt = new Date(post.created_at)
  const updatedAt = new Date(post.updated_at)
  const shouldShowUpdatedAt = (now - updatedAt) < (now - createdAt) * 0.8

  return

    <A to={`/p/${post.id}`}>{humanDate(createdAt).replace(/ /g, String.fromCharCode(160))}
    {shouldShowUpdatedAt &&
      {spacer}updated {humanDate(updatedAt).replace(/ /g, String.fromCharCode(160))}
    }
    {spacer}
    {post.votes} ♡
    {spacer}

      {post.numComments} comment{post.numComments === 1 ? '' : 's'}

    {post.public &&
      {spacer}Public
    }

}

let share = href => {
  console.log('Sharing', href)
  FB.ui({
    method: 'share',
    href: href
  }, function (response) {})
}

const ExpandedPostDetails = props => {
  let {
    post, image, comments, commentsExpanded, currentUser, dispatch,
    commentingDisabled, onCommentCreate, communities
  } = props
  let description = present(sanitize(post.description))
  let attachments = filter(post.media, m => m.type !== 'image')

  let origin = typeof window !== 'undefined' ? window.location.origin : 'https://www.hylo.com'
  let postUrl = `${origin}/p/${post.id}`

  if (!comments) comments = []

  return

    {image && }

    {description && <ClickCatchingDiv className='details post-section'
      dangerouslySetInnerHTML={{__html: description}}/>}

    {post.type === 'event' && <RSVPControl
      responders={post.responders}
      currentResponse={(find(post.responders, responder => responder.id === currentUser.id) || {response: ''}).response}
      onPickResponse={choice => dispatch(changeEventResponse(post.id, choice, currentUser))} />}

    {!isEmpty(attachments) &&

      {attachments.map((file, i) =>

          {truncate(file.name, 40)}
        )}

}

    {post.public &&

      <Dropdown
        toggleChildren={
Share
}>

 share(postUrl)}>Facebook

Twitter

      </Dropdown>

}

{post.type}

        {communities.map(c =>

          <Link to={`/c/${c.slug}`} key={c.id}>{c.name}</Link>

)}

    {commentsExpanded &&

      {comments.map(c => <Comment comment={c} key={c.id}/>)}
      {!commentingDisabled && <CommentForm onCreate={onCommentCreate}/>}

}

}
<|endoftext|>


<commit_before>import R from 'ramda';
import path from 'path';
import { assert } from 'chai';
import { listPatches } from 'xod-project';
import { loadProject } from 'xod-fs';

import { createIndex, createIndexData } from '../src/index';

const workspace = path.resolve(__dirname, '../../../workspace');
const getProjectPath = projectName => path.resolve(workspace, projectName);
const fixture = p => path.resolve(__dirname, './fixtures/', p);

describe('xod-patch-search/index', () => {
  describe('general search', () => {
    let indexData = [];
    let idx = {};
    before(() =>
      loadProject([workspace], getProjectPath('welcome-to-xod'))
        .then(listPatches)
        .then(createIndexData)
        .then(iData => {
          indexData = iData;
          idx = createIndex(indexData);
        })
    );

    it('searches by path correctly', () => {
      const result = idx.search('path:number');
      assert.equal(result[0].item.path, 'xod/core/nth-number');
    });

    it('searches by lib correctly', () => {
      const result = idx.search('lib:xod/core');
      const expectedLength = R.compose(
        R.length,
        // We have to reject patches with path contains "(",
        // cause we're not showing specialization nodes if
        // query does not contain "(" symbol
        R.reject(R.propSatisfies(R.contains('('), 'path')),
        R.filter(R.propEq('lib', 'xod/core'))
      )(indexData);
      assert.lengthOf(result, expectedLength);
    });

    it('searches: "number"', () =>
      assert.equal(
        idx.search('number')[0].item.path,
        'xod/core/nth-number' // Cause it has a `number` in the path and it alphabetically sorted (that's why not *-to-string)
      ));

    it('searches: "lib:xod/patch-nodes number"', () => {
      assert.equal(
        idx.search('lib:xod/patch-nodes number')[0].item.path,
        'xod/patch-nodes/input-number' // The same as above + filtered by lib
      );
    });

    it('searches: "meter"', () => {
      const results = idx.search('meter');
      const foundPatchPaths = R.map(R.path(['item', 'path']), results);
      assert.equal(
        results[0].item.path,
        'xod/common-hardware/gp2y0a02-range-meter' // Cause this node has a full match `meter` in the `path`
      );
      assert.includeMembers(foundPatchPaths, ['xod/units/m-to-ft']); // Don't care about position, but be sure that it's found
    });

    it('searches: "temp"', () =>
      assert.equal(
        idx.search('temp')[0].item.path,
        'xod/units/c-to-f' // Cause this node has a `temperature` in the description
      ));

    it('searches: "therm"', () => {
      const results = idx.search('therm');
      assert.equal(
        results[0].item.path,
        'xod/common-hardware/thermometer-tmp36' // Cause this node has a `thermometer` in the path and it alphabetically sorted
      );
      assert.equal(
        results[1].item.path,
        'xod/common-hardware/dht11-thermometer' // Cause this node has a `thermometer` in the path and it alphabetically sorted
      );
    });

    it('searches: "ult"', () => {
      const results = idx.search('ult');
      assert.equal(
        results[0].item.path,
        'xod/common-hardware/hc-sr04-ultrasonic-range'
      );
      assert.equal(
        results[1].item.path,
        'xod/common-hardware/hc-sr04-ultrasonic-time'
      );
      assert.equal(results[2].item.path, 'xod/core/multiply');
    });
  });

  describe('abstract nodes and specializations', () => {
    let indexData = [];
    let idx = {};
    before(() =>
      loadProject([workspace], fixture('abstract-and-specializations.xodball'))
        .then(listPatches)
        .then(createIndexData)
        .then(iData => {
          indexData = iData;
          idx = createIndex(indexData);
        })
    );
    it('searches abstract node "when-either-ch" and doesn\'t show specialization nodes', () => {
      const results = idx.search('when-either-ch');
      const patchPaths = R.map(R.path(['item', 'path']), results);

      assert.equal(results[0].item.path, '@/when-either-changes');
      assert.isFalse(
        R.contains('@/when-either-changes(number,string)', patchPaths)
      );
      assert.isFalse(
        R.contains('@/when-either-changes(number,boolean)', patchPaths)
      );
    });
    it('searches all specialization nodes for "when-either-ch("', () => {
      const results = idx.search('when-either-ch(');
      const patchPaths = R.map(R.path(['item', 'path']), results);

      assert.isFalse(R.contains('@/when-either-changes', patchPaths));
      assert.isTrue(
        R.contains('@/when-either-changes(number,string)', patchPaths)
      );
      assert.isTrue(
        R.contains('@/when-either-changes(number,boolean)', patchPaths)
      );
    });
    it('searches both specialization nodes for "when-either-ch(mber"', () => {
      const results = idx.search('when-either-ch(mber');
      const patchPaths = R.map(R.path(['item', 'path']), results);
      const specializations = R.take(2, patchPaths);

      assert.isFalse(R.contains('@/when-either-changes', patchPaths));
      assert.sameMembers(
        [
          '@/when-either-changes(number,string)',
          '@/when-either-changes(number,boolean)',
        ],
        specializations
      );
    });
    it('searches one specialization node for "when-(tr"', () => {
      const results = idx.search('when-(tr');

      assert.equal(
        results[0].item.path,
        '@/when-either-changes(number,string)'
      );
    });
    it('searches one specialization node for "when-either-ch(ring"', () => {
      const results = idx.search('when-either-ch(ring');
      const patchPaths = R.map(R.path(['item', 'path']), results);

      assert.equal(
        results[0].item.path,
        '@/when-either-changes(number,string)'
      );
      assert.isFalse(R.contains('@/when-either-changes', patchPaths));
    });
    it('searches one specialization node for "when-either-ch(number,boolean)"', () => {
      const results = idx.search('when-either-ch(number,boolean)');

      assert.equal(
        results[0].item.path,
        '@/when-either-changes(number,boolean)'
      );
    });
    it('searches no specialization node for "when-either-ch(pulse)"', () => {
      const results = idx.search('when-either-ch(pulse)');

      assert.lengthOf(results, 0);
    });
  });
});
<commit_msg>fix(xod-patch-search): fix test<commit_after>import R from 'ramda';
import path from 'path';
import { assert } from 'chai';
import { listPatches } from 'xod-project';
import { loadProject } from 'xod-fs';

import { createIndex, createIndexData } from '../src/index';

const workspace = path.resolve(__dirname, '../../../workspace');
const getProjectPath = projectName => path.resolve(workspace, projectName);
const fixture = p => path.resolve(__dirname, './fixtures/', p);

describe('xod-patch-search/index', () => {
  describe('general search', () => {
    let indexData = [];
    let idx = {};
    before(() =>
      loadProject([workspace], getProjectPath('welcome-to-xod'))
        .then(listPatches)
        .then(createIndexData)
        .then(iData => {
          indexData = iData;
          idx = createIndex(indexData);
        })
    );

    it('searches by path correctly', () => {
      const result = idx.search('path:number');
      assert.equal(result[0].item.path, 'xod/core/nth-number');
    });

    it('searches by lib correctly', () => {
      const result = idx.search('lib:xod/core');
      const expectedLength = R.compose(
        R.length,
        // We have to reject patches with path contains "(",
        // cause we're not showing specialization nodes if
        // query does not contain "(" symbol
        R.reject(R.propSatisfies(R.contains('('), 'path')),
        R.filter(R.propEq('lib', 'xod/core'))
      )(indexData);
      assert.lengthOf(result, expectedLength);
    });

    it('searches: "number"', () =>
      assert.equal(
        idx.search('number')[0].item.path,
        'xod/bits/number-to-f32'
      ));

    it('searches: "lib:xod/patch-nodes number"', () => {
      assert.equal(
        idx.search('lib:xod/patch-nodes number')[0].item.path,
        'xod/patch-nodes/input-number' // The same as above + filtered by lib
      );
    });

    it('searches: "meter"', () => {
      const results = idx.search('meter');
      const foundPatchPaths = R.map(R.path(['item', 'path']), results);
      assert.equal(
        results[0].item.path,
        'xod/common-hardware/gp2y0a02-range-meter' // Cause this node has a full match `meter` in the `path`
      );
      assert.includeMembers(foundPatchPaths, ['xod/units/m-to-ft']); // Don't care about position, but be sure that it's found
    });

    it('searches: "temp"', () =>
      assert.equal(
        idx.search('temp')[0].item.path,
        'xod/units/c-to-f' // Cause this node has a `temperature` in the description
      ));

    it('searches: "therm"', () => {
      const results = idx.search('therm');
      assert.equal(
        results[0].item.path,
        'xod/common-hardware/thermometer-tmp36' // Cause this node has a `thermometer` in the path and it alphabetically sorted
      );
      assert.equal(
        results[1].item.path,
        'xod/common-hardware/dht11-thermometer' // Cause this node has a `thermometer` in the path and it alphabetically sorted
      );
    });

    it('searches: "ult"', () => {
      const results = idx.search('ult');
      assert.equal(
        results[0].item.path,
        'xod/common-hardware/hc-sr04-ultrasonic-range'
      );
      assert.equal(
        results[1].item.path,
        'xod/common-hardware/hc-sr04-ultrasonic-time'
      );
      assert.equal(results[2].item.path, 'xod/core/multiply');
    });
  });

  describe('abstract nodes and specializations', () => {
    let indexData = [];
    let idx = {};
    before(() =>
      loadProject([workspace], fixture('abstract-and-specializations.xodball'))
        .then(listPatches)
        .then(createIndexData)
        .then(iData => {
          indexData = iData;
          idx = createIndex(indexData);
        })
    );
    it('searches abstract node "when-either-ch" and doesn\'t show specialization nodes', () => {
      const results = idx.search('when-either-ch');
      const patchPaths = R.map(R.path(['item', 'path']), results);

      assert.equal(results[0].item.path, '@/when-either-changes');
      assert.isFalse(
        R.contains('@/when-either-changes(number,string)', patchPaths)
      );
      assert.isFalse(
        R.contains('@/when-either-changes(number,boolean)', patchPaths)
      );
    });
    it('searches all specialization nodes for "when-either-ch("', () => {
      const results = idx.search('when-either-ch(');
      const patchPaths = R.map(R.path(['item', 'path']), results);

      assert.isFalse(R.contains('@/when-either-changes', patchPaths));
      assert.isTrue(
        R.contains('@/when-either-changes(number,string)', patchPaths)
      );
      assert.isTrue(
        R.contains('@/when-either-changes(number,boolean)', patchPaths)
      );
    });
    it('searches both specialization nodes for "when-either-ch(mber"', () => {
      const results = idx.search('when-either-ch(mber');
      const patchPaths = R.map(R.path(['item', 'path']), results);
      const specializations = R.take(2, patchPaths);

      assert.isFalse(R.contains('@/when-either-changes', patchPaths));
      assert.sameMembers(
        [
          '@/when-either-changes(number,string)',
          '@/when-either-changes(number,boolean)',
        ],
        specializations
      );
    });
    it('searches one specialization node for "when-(tr"', () => {
      const results = idx.search('when-(tr');

      assert.equal(
        results[0].item.path,
        '@/when-either-changes(number,string)'
      );
    });
    it('searches one specialization node for "when-either-ch(ring"', () => {
      const results = idx.search('when-either-ch(ring');
      const patchPaths = R.map(R.path(['item', 'path']), results);

      assert.equal(
        results[0].item.path,
        '@/when-either-changes(number,string)'
      );
      assert.isFalse(R.contains('@/when-either-changes', patchPaths));
    });
    it('searches one specialization node for "when-either-ch(number,boolean)"', () => {
      const results = idx.search('when-either-ch(number,boolean)');

      assert.equal(
        results[0].item.path,
        '@/when-either-changes(number,boolean)'
      );
    });
    it('searches no specialization node for "when-either-ch(pulse)"', () => {
      const results = idx.search('when-either-ch(pulse)');

      assert.lengthOf(results, 0);
    });
  });
});
<|endoftext|>


<commit_before>const _ = require('lodash');
const url = require('url');
const httpProxy = require('http-proxy');
const headers = require('./lib/headers');
const cookie = require('cookie');
const Cookie = require('tough-cookie').Cookie;
const harmon = require('harmon');
const srcset = require('srcset');
const cssUrlPattern = /url\(\s*(['"])((?:\\[\s\S]|(?!\1).)*)\1\s*\)|url\(((?:\\[\s\S]|[^)])*)\)/gi;

module.exports = (proxyHost, host) => {
	let proxy = httpProxy.createProxyServer();
	let proxyUrl = url.parse(proxyHost, false, true);
	let rewriteAttributes = [ 'action', 'href', 'link', 'src', 'srcset', 'style' ];

	proxy.on('proxyReq', (proxyReq, req) => {
		// Only forward standard headers.
		_.forEach(proxyReq.getHeaders(), (value, key) => {
			if (!headers.isRequestHeader(key)) {
				proxyReq.removeHeader(key);
			}
		});

		// Remove Cloudflare cookies.
		if (proxyReq.getHeader('cookie')) {
			let cookies = _.omit(cookie.parse(proxyReq.getHeader('cookie')), '__cfduid');
			proxyReq.setHeader('cookie', _.map(cookies, (value, name) => cookie.serialize(name, value)).join('; '));
		}

		proxyReq.setHeader('X-Fowarded-For', req.ip);
	});

	proxy.on('proxyRes', (proxyRes) => {
		// Only forward standard headers.
		_.forEach(proxyRes.headers, (value, key) => {
			if (!headers.isResponseHeader(key)) {
				delete proxyRes.headers[key];
			}
		});

		// Remove Cloudflare cookies.
		if (proxyRes.headers['set-cookie']) {
			proxyRes.headers['set-cookie'] = proxyRes.headers['set-cookie'].filter((string) => {
				return Cookie.parse(string).key !== '__cfduid';
			});

			if (!proxyRes.headers['set-cookie'].length) {
				delete proxyRes.headers['set-cookie'];
			}
		}

		// Rewrite redirects.
		if (proxyRes.headers.location) {
			let location = url.parse(proxyRes.headers.location, false, true);

			if (matchesHost(location, proxyUrl.host)) {
				return proxyRes.headers.location = host + '/blog' + location.path;
			}
		}
	});

	return [
		/**
		 * Rewrite links in HTML.
		 */
		harmon([], rewriteAttributes.map((name) => {
			return {
				query: `[${name}]`,
				func (el) {
					let rewrite = (value) => {
						let link = url.parse(value, false, true);

						if (matchesHost(link, proxyUrl.host)) {
							return host + '/blog' + link.pathname;
						}

						return value;
					};

					el.getAttribute(name, (value) => {
						try {
							if (name === 'srcset') {
								value = srcset.stringify(srcset.parse(value).map(src => (src.url = rewrite(src.url), src)));
							} else if (name === 'style') {
								value = value.replace(cssUrlPattern, ($0, $1, $2, $3) => {
									return `url("${rewrite($2 || $3)}")`;
								});
							} else {
								value = rewrite(value);
							}

							el.setAttribute(name, value);
						} catch (e) {}
					});
				},
			};
		}), true),

		/**
		 * Fix for harmon with http-proxy@1.17.0+
		 * Harmon assumes writeHead() is called before write() which is not the case anymore - http-proxy doesn't call writeHead() at all, it's called by node code from write().
		 */
		(req, res, next) => {
			let write = res.write;

			res.write = function (...args) {
				if (!this.headersSent) {
					this.writeHead(this.statusCode);
				}

				return write.apply(this, args);
			};

			return next();
		},

		/**
		 * The main proxy middleware.
		 */
		(req, res, next) => {
			res.status(502);

			proxy.web(req, res, {
				target: proxyHost,
				changeOrigin: true,
				protocolRewrite: 'https',
				cookieDomainRewrite: '',
				cookiePathRewrite: req.baseUrl,
				proxyTimeout: 10000,
			}, next);
		},
	];
};

function matchesHost (url, host) {
	return (!url.host && url.pathname.charAt(0) === '/') || url.host === host;
}
<commit_msg>Update proxy.js<commit_after>const _ = require('lodash');
const url = require('url');
const httpProxy = require('http-proxy');
const headers = require('./lib/headers');
const cookie = require('cookie');
const Cookie = require('tough-cookie').Cookie;
const harmon = require('harmon');
const srcset = require('srcset');
const cssUrlPattern = /url\(\s*(['"])((?:\\[\s\S]|(?!\1).)*)\1\s*\)|url\(((?:\\[\s\S]|[^)])*)\)/gi;

module.exports = (proxyHost, host) => {
	let proxy = httpProxy.createProxyServer();
	let proxyUrl = url.parse(proxyHost, false, true);
	let rewriteAttributes = [ 'action', 'href', 'link', 'src', 'srcset', 'style' ];

	proxy.on('proxyReq', (proxyReq, req) => {
		// Only forward standard headers.
		_.forEach(proxyReq.getHeaders(), (value, key) => {
			if (!headers.isRequestHeader(key)) {
				proxyReq.removeHeader(key);
			}
		});

		// Remove Cloudflare cookies.
		if (proxyReq.getHeader('cookie')) {
			let cookies = _.omit(cookie.parse(proxyReq.getHeader('cookie')), '__cfduid');
			proxyReq.setHeader('cookie', _.map(cookies, (value, name) => cookie.serialize(name, value)).join('; '));
		}

		proxyReq.setHeader('X-Fowarded-For', req.ip);
	});

	proxy.on('proxyRes', (proxyRes, req) => {
		// Only forward standard headers.
		_.forEach(proxyRes.headers, (value, key) => {
			if (!headers.isResponseHeader(key)) {
				delete proxyRes.headers[key];
			}
		});

		// Remove Cloudflare cookies.
		if (proxyRes.headers['set-cookie']) {
			proxyRes.headers['set-cookie'] = proxyRes.headers['set-cookie'].filter((string) => {
				return Cookie.parse(string).key !== '__cfduid';
			});

			if (!proxyRes.headers['set-cookie'].length) {
				delete proxyRes.headers['set-cookie'];
			}
		}

		// Rewrite redirects.
		if (proxyRes.headers.location) {
			let location = url.parse(proxyRes.headers.location, false, true);

			if (matchesHost(location, proxyUrl.host)) {
				return proxyRes.headers.location = host + req.baseUrl + location.path;
			}
		}
	});

	return [
		/**
		 * Rewrite links in HTML.
		 */
		harmon([], rewriteAttributes.map((name) => {
			return {
				query: `[${name}]`,
				func (el, req) {
					let rewrite = (value) => {
						let link = url.parse(value, false, true);

						if (matchesHost(link, proxyUrl.host)) {
							return host + req.baseUrl + link.pathname;
						}

						return value;
					};

					el.getAttribute(name, (value) => {
						try {
							if (name === 'srcset') {
								value = srcset.stringify(srcset.parse(value).map(src => (src.url = rewrite(src.url), src)));
							} else if (name === 'style') {
								value = value.replace(cssUrlPattern, ($0, $1, $2, $3) => {
									return `url("${rewrite($2 || $3)}")`;
								});
							} else {
								value = rewrite(value);
							}

							el.setAttribute(name, value);
						} catch (e) {}
					});
				},
			};
		}), true),

		/**
		 * Fix for harmon with http-proxy@1.17.0+
		 * Harmon assumes writeHead() is called before write() which is not the case anymore - http-proxy doesn't call writeHead() at all, it's called by node code from write().
		 */
		(req, res, next) => {
			let write = res.write;

			res.write = function (...args) {
				if (!this.headersSent) {
					this.writeHead(this.statusCode);
				}

				return write.apply(this, args);
			};

			return next();
		},

		/**
		 * The main proxy middleware.
		 */
		(req, res, next) => {
			res.status(502);

			proxy.web(req, res, {
				target: proxyHost,
				changeOrigin: true,
				protocolRewrite: 'https',
				cookieDomainRewrite: '',
				cookiePathRewrite: req.baseUrl,
				proxyTimeout: 10000,
			}, next);
		},
	];
};

function matchesHost (url, host) {
	return (!url.host && url.pathname.charAt(0) === '/') || url.host === host;
}
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import colors from '../constants/colors';
import TicketController from './TicketController';
import CTAButton from './Button';
import Currency from './Currency';
import { defineMessages, FormattedNumber, FormattedMessage } from 'react-intl';
import withIntl from '../lib/withIntl';
import { ButtonGroup, Button } from 'react-bootstrap';
import InputField from './InputField';
import { getCurrencySymbol, capitalize, formatCurrency } from '../lib/utils';
import { get } from 'lodash';
import Markdown from 'react-markdown';

class Tier extends React.Component {
  static propTypes = {
    tier: PropTypes.object.isRequired,
    className: PropTypes.string,
    values: PropTypes.object, // overriding values {quantity, amount, interval}
    onChange: PropTypes.func, // onChange({ id, quantity, amount, interval })
    onClick: PropTypes.func, // onClick({ id, quantity, amount, interval })
  };

  constructor(props) {
    super(props);
    this.state = { customAmountError: null };

    this.onChange = this.props.onChange || function() {};
    this.onClick = this.props.onClick || function() {};
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);

    this.currencyStyle = {
      style: 'currency',
      currencyDisplay: 'symbol',
      minimumFractionDigits: 0,
      maximumFractionDigits: 2,
    };

    this.messages = defineMessages({
      'amount.label': { id: 'tier.amount.label', defaultMessage: 'amount' },
      'customAmount.error.minimumAmount': {
        id: 'tier.customAmount.error.minimumAmount',
        defaultMessage: 'Minimum amount should be {minimumAmount}',
      },
      'interval.label': {
        id: 'tier.interval.label',
        defaultMessage: 'interval',
      },
      'interval.onetime': {
        id: 'tier.interval.onetime',
        defaultMessage: 'one time',
      },
      'interval.month': { id: 'tier.interval.month', defaultMessage: 'month' },
      'interval.year': { id: 'tier.interval.year', defaultMessage: 'year' },
    });

    this.defaultDescription = (
      <FormattedMessage
        id="tier.defaultDescription"
        defaultMessage="Become a {name} for {amount} per {interval} and help us sustain our activities!"
        values={{
          name: props.tier.name,
          amount: formatCurrency(props.tier.amount, props.tier.currency),
          interval: props.tier.interval,
        }}
      />
    );

    switch (props.tier.type) {
      case 'DONATION':
        this.buttonLabel = (
          <FormattedMessage id="tier.donation.button" defaultMessage="donate" />
        );
        this.defaultDescription = (
          <FormattedMessage
            id="tier.donation.description"
            defaultMessage="Thank you for your kind donation 🙏"
          />
        );
        break;
      case 'TIER':
      default:
        this.buttonLabel = (
          <FormattedMessage
            id="tier.button"
            values={{ name: this.props.tier.name }}
            defaultMessage="become a {name}"
          />
        );
        break;
    }
  }

  componentDidMount() {
    const currentValues = this.calcCurrentValues();
    const values = this.props.values;

    // handle the initial condition of starting without an amount
    if (currentValues.amount && (values && !values.amount)) {
      this.handleChange('amount', currentValues.amount);
    }
  }

  // since this is a pure component, we don't want to store state
  // But, we still need a way to construct the current values of
  // quantity, amount and interval
  calcCurrentValues() {
    const { values, tier } = this.props;

    let quantity, amount, singleAmount, interval, presets;

    // Case 1: handle presets. Both interval and amount are changeable
    if (tier.presets) {
      presets = tier.presets.filter(p => !isNaN(p)).map(p => parseInt(p, 10));
      interval = (values && values.interval) || null;
      amount =
        (values && values.amount) || presets[Math.floor(presets.length / 2)];
      quantity = 1;
    } else if (tier.type === 'TICKET') {
      // Case 2: handle quantity. Can't be active at same time as presets
      quantity = (values && values.quantity) || 1;
      singleAmount = tier.amount;
      amount = tier.amount * quantity;
    } else if (tier.amount || values.amount) {
      // Case 3: nothing is changeable, comes with amount (and interval optional)
      interval = tier.interval || values.interval;
      amount = tier.amount || values.amount;
      quantity = 1;
    }
    return { interval, amount, singleAmount, quantity, presets };
  }

  handleTicketsChange(quantity) {
    const currentValues = this.calcCurrentValues();

    const response = {
      ...this.props.tier,
      quantity,
    };
    if (currentValues.interval) {
      response.interval = currentValues.interval;
    }
    this.onChange(response);
  }

  handleChange(field, value) {
    const { tier, intl } = this.props;

    const currentValues = this.calcCurrentValues();

    const response = Object.assign({}, tier, {
      amount: currentValues.amount,
      interval: currentValues.interval,
      quantity: currentValues.quantity,
    });

    // Make sure that the custom amount entered by the user is never under the minimum preset amount
    if (field === 'amount') {
      if (tier.presets && tier.presets[0] > value) {
        value = tier.presets[0];
        this.setState({
          customAmountError: intl.formatMessage(
            this.messages['customAmount.error.minimumAmount'],
            { minimumAmount: formatCurrency(value, tier.currency) },
          ),
        });
      } else {
        this.setState({ customAmountError: null });
      }
    }

    response[field] = value;
    this.onChange(response);
  }

  handleSubmit() {
    const { tier } = this.props;

    const {
      quantity,
      amount,
      singleAmount,
      interval,
    } = this.calcCurrentValues();

    this.onClick({ id: tier.id, amount, singleAmount, quantity, interval });
  }

  render() {
    const { intl, tier } = this.props;
    const { type, name, description, currency, maxQuantity } = this.props.tier;

    const intervals = [null, 'month', 'year'];
    const currentValues = this.calcCurrentValues();
    const { quantity, amount, interval, presets } = currentValues;
    const anchor = (get(tier, 'name') || '').toLowerCase().replace(/ /g, '-');

    if (type === 'TICKET') {
      this.buttonLabel = (
        <FormattedMessage
          id="tier.ticket.button"
          values={{ quantity }}
          defaultMessage="{quantity, plural, one {get ticket} other {get tickets}}"
        />
      );
    }

    return (

{capitalize(name)}

            {!presets && (

                {!amount &&
                  tier.type !== 'DONATION' && (
                    <FormattedMessage id="amount.free" defaultMessage="free" />
                  )}
                {amount > 0 && <Currency value={amount} currency={currency} />}
                {interval && (

                    <FormattedMessage
                      id="tier.interval"
                      defaultMessage="per {interval, select, month {month} year {year} other {}}"
                      values={{ interval }}
                    />

                )}

            )}

          {maxQuantity > 0 && (

              <FormattedMessage
                id="tier.limited"
                values={{
                  maxQuantity,
                  availableQuantity: get(tier, 'stats.availableQuantity'),
                }}
                defaultMessage="LIMITED: {availableQuantity} LEFT OUT OF {maxQuantity}"
              />

          )}

            {description && <Markdown source={description} />}
            {!description &&
{this.defaultDescription}
}

            {presets && (

                    <FormattedMessage
                      id="tier.amount.select"
                      defaultMessage="Select {interval, select, month {monthly} year {yearly} other {one time}} amount"
                      values={{ interval }}
                    />

                    <ButtonGroup className="presetBtnGroup">
                      {presets.map(
                        preset =>
                          !isNaN(preset) && (
                            <Button
                              key={`presetBtn-${preset}`}
                              className="presetBtn"
                              bsStyle={
                                amount === preset ? 'primary' : 'default'
                              }
                              onClick={() =>
                                this.handleChange('amount', preset)
                              }
                            >
                              <FormattedNumber
                                value={preset / 100}
                                currency={currency}
                                {...this.currencyStyle}
                              />
                            </Button>
                          ),
                      )}
                    </ButtonGroup>

                      <InputField
                        name="amount"
                        className="inputAmount"
                        min={tier.presets && tier.presets[0]}
                        pre={getCurrencySymbol(currency)}
                        type="currency"
                        value={amount}
                        onChange={amount => this.handleChange('amount', amount)}
                      />
                      {this.state.customAmountError && (

                          {this.state.customAmountError}

                      )}

                {type === 'DONATION' && (

                      <FormattedMessage
                        id="tier.interval.select"
                        defaultMessage="Select frequency"
                      />

                    <ButtonGroup className="intervalBtnGroup">
                      {intervals.map(i => (
                        <Button
                          key={`key-${i}`}
                          className={`intervalBtn ${i}`}
                          bsStyle={interval === i ? 'primary' : 'default'}
                          onClick={() => this.handleChange('interval', i)}
                        >
                          {intl.formatMessage(
                            this.messages[`interval.${i || 'onetime'}`],
                          )}
                        </Button>
                      ))}
                    </ButtonGroup>

                )}

            )}

          {type === 'TICKET' && (

              <TicketController
                value={quantity}
                onChange={value => this.handleTicketsChange(value)}
              />
              {this.props.onClick && (
                <CTAButton
                  className="ctabtn blue ticket"
                  label={this.buttonLabel}
                  onClick={this.handleSubmit}
                />
              )}

          )}
          {type !== 'TICKET' &&
            this.props.onClick && (

                <CTAButton
                  className="ctabtn blue"
                  label={tier.button || this.buttonLabel}
                  onClick={this.handleSubmit}
                />

            )}

    );
  }
}

export default withIntl(Tier);
<commit_msg>Increase donate customAmount input size<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import colors from '../constants/colors';
import TicketController from './TicketController';
import CTAButton from './Button';
import Currency from './Currency';
import { defineMessages, FormattedNumber, FormattedMessage } from 'react-intl';
import withIntl from '../lib/withIntl';
import { ButtonGroup, Button } from 'react-bootstrap';
import InputField from './InputField';
import { getCurrencySymbol, capitalize, formatCurrency } from '../lib/utils';
import { get } from 'lodash';
import Markdown from 'react-markdown';

class Tier extends React.Component {
  static propTypes = {
    tier: PropTypes.object.isRequired,
    className: PropTypes.string,
    values: PropTypes.object, // overriding values {quantity, amount, interval}
    onChange: PropTypes.func, // onChange({ id, quantity, amount, interval })
    onClick: PropTypes.func, // onClick({ id, quantity, amount, interval })
  };

  constructor(props) {
    super(props);
    this.state = { customAmountError: null };

    this.onChange = this.props.onChange || function() {};
    this.onClick = this.props.onClick || function() {};
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);

    this.currencyStyle = {
      style: 'currency',
      currencyDisplay: 'symbol',
      minimumFractionDigits: 0,
      maximumFractionDigits: 2,
    };

    this.messages = defineMessages({
      'amount.label': { id: 'tier.amount.label', defaultMessage: 'amount' },
      'customAmount.error.minimumAmount': {
        id: 'tier.customAmount.error.minimumAmount',
        defaultMessage: 'Minimum amount should be {minimumAmount}',
      },
      'interval.label': {
        id: 'tier.interval.label',
        defaultMessage: 'interval',
      },
      'interval.onetime': {
        id: 'tier.interval.onetime',
        defaultMessage: 'one time',
      },
      'interval.month': { id: 'tier.interval.month', defaultMessage: 'month' },
      'interval.year': { id: 'tier.interval.year', defaultMessage: 'year' },
    });

    this.defaultDescription = (
      <FormattedMessage
        id="tier.defaultDescription"
        defaultMessage="Become a {name} for {amount} per {interval} and help us sustain our activities!"
        values={{
          name: props.tier.name,
          amount: formatCurrency(props.tier.amount, props.tier.currency),
          interval: props.tier.interval,
        }}
      />
    );

    switch (props.tier.type) {
      case 'DONATION':
        this.buttonLabel = (
          <FormattedMessage id="tier.donation.button" defaultMessage="donate" />
        );
        this.defaultDescription = (
          <FormattedMessage
            id="tier.donation.description"
            defaultMessage="Thank you for your kind donation 🙏"
          />
        );
        break;
      case 'TIER':
      default:
        this.buttonLabel = (
          <FormattedMessage
            id="tier.button"
            values={{ name: this.props.tier.name }}
            defaultMessage="become a {name}"
          />
        );
        break;
    }
  }

  componentDidMount() {
    const currentValues = this.calcCurrentValues();
    const values = this.props.values;

    // handle the initial condition of starting without an amount
    if (currentValues.amount && (values && !values.amount)) {
      this.handleChange('amount', currentValues.amount);
    }
  }

  // since this is a pure component, we don't want to store state
  // But, we still need a way to construct the current values of
  // quantity, amount and interval
  calcCurrentValues() {
    const { values, tier } = this.props;

    let quantity, amount, singleAmount, interval, presets;

    // Case 1: handle presets. Both interval and amount are changeable
    if (tier.presets) {
      presets = tier.presets.filter(p => !isNaN(p)).map(p => parseInt(p, 10));
      interval = (values && values.interval) || null;
      amount =
        (values && values.amount) || presets[Math.floor(presets.length / 2)];
      quantity = 1;
    } else if (tier.type === 'TICKET') {
      // Case 2: handle quantity. Can't be active at same time as presets
      quantity = (values && values.quantity) || 1;
      singleAmount = tier.amount;
      amount = tier.amount * quantity;
    } else if (tier.amount || values.amount) {
      // Case 3: nothing is changeable, comes with amount (and interval optional)
      interval = tier.interval || values.interval;
      amount = tier.amount || values.amount;
      quantity = 1;
    }
    return { interval, amount, singleAmount, quantity, presets };
  }

  handleTicketsChange(quantity) {
    const currentValues = this.calcCurrentValues();

    const response = {
      ...this.props.tier,
      quantity,
    };
    if (currentValues.interval) {
      response.interval = currentValues.interval;
    }
    this.onChange(response);
  }

  handleChange(field, value) {
    const { tier, intl } = this.props;

    const currentValues = this.calcCurrentValues();

    const response = Object.assign({}, tier, {
      amount: currentValues.amount,
      interval: currentValues.interval,
      quantity: currentValues.quantity,
    });

    // Make sure that the custom amount entered by the user is never under the minimum preset amount
    if (field === 'amount') {
      if (tier.presets && tier.presets[0] > value) {
        value = tier.presets[0];
        this.setState({
          customAmountError: intl.formatMessage(
            this.messages['customAmount.error.minimumAmount'],
            { minimumAmount: formatCurrency(value, tier.currency) },
          ),
        });
      } else {
        this.setState({ customAmountError: null });
      }
    }

    response[field] = value;
    this.onChange(response);
  }

  handleSubmit() {
    const { tier } = this.props;

    const {
      quantity,
      amount,
      singleAmount,
      interval,
    } = this.calcCurrentValues();

    this.onClick({ id: tier.id, amount, singleAmount, quantity, interval });
  }

  render() {
    const { intl, tier } = this.props;
    const { type, name, description, currency, maxQuantity } = this.props.tier;

    const intervals = [null, 'month', 'year'];
    const currentValues = this.calcCurrentValues();
    const { quantity, amount, interval, presets } = currentValues;
    const anchor = (get(tier, 'name') || '').toLowerCase().replace(/ /g, '-');

    if (type === 'TICKET') {
      this.buttonLabel = (
        <FormattedMessage
          id="tier.ticket.button"
          values={{ quantity }}
          defaultMessage="{quantity, plural, one {get ticket} other {get tickets}}"
        />
      );
    }

    return (

{capitalize(name)}

            {!presets && (

                {!amount &&
                  tier.type !== 'DONATION' && (
                    <FormattedMessage id="amount.free" defaultMessage="free" />
                  )}
                {amount > 0 && <Currency value={amount} currency={currency} />}
                {interval && (

                    <FormattedMessage
                      id="tier.interval"
                      defaultMessage="per {interval, select, month {month} year {year} other {}}"
                      values={{ interval }}
                    />

                )}

            )}

          {maxQuantity > 0 && (

              <FormattedMessage
                id="tier.limited"
                values={{
                  maxQuantity,
                  availableQuantity: get(tier, 'stats.availableQuantity'),
                }}
                defaultMessage="LIMITED: {availableQuantity} LEFT OUT OF {maxQuantity}"
              />

          )}

            {description && <Markdown source={description} />}
            {!description &&
{this.defaultDescription}
}

            {presets && (

                    <FormattedMessage
                      id="tier.amount.select"
                      defaultMessage="Select {interval, select, month {monthly} year {yearly} other {one time}} amount"
                      values={{ interval }}
                    />

                    <ButtonGroup className="presetBtnGroup">
                      {presets.map(
                        preset =>
                          !isNaN(preset) && (
                            <Button
                              key={`presetBtn-${preset}`}
                              className="presetBtn"
                              bsStyle={
                                amount === preset ? 'primary' : 'default'
                              }
                              onClick={() =>
                                this.handleChange('amount', preset)
                              }
                            >
                              <FormattedNumber
                                value={preset / 100}
                                currency={currency}
                                {...this.currencyStyle}
                              />
                            </Button>
                          ),
                      )}
                    </ButtonGroup>

                      <InputField
                        name="amount"
                        className="inputAmount"
                        min={tier.presets && tier.presets[0]}
                        pre={getCurrencySymbol(currency)}
                        type="currency"
                        value={amount}
                        onChange={amount => this.handleChange('amount', amount)}
                      />
                      {this.state.customAmountError && (

                          {this.state.customAmountError}

                      )}

                {type === 'DONATION' && (

                      <FormattedMessage
                        id="tier.interval.select"
                        defaultMessage="Select frequency"
                      />

                    <ButtonGroup className="intervalBtnGroup">
                      {intervals.map(i => (
                        <Button
                          key={`key-${i}`}
                          className={`intervalBtn ${i}`}
                          bsStyle={interval === i ? 'primary' : 'default'}
                          onClick={() => this.handleChange('interval', i)}
                        >
                          {intl.formatMessage(
                            this.messages[`interval.${i || 'onetime'}`],
                          )}
                        </Button>
                      ))}
                    </ButtonGroup>

                )}

            )}

          {type === 'TICKET' && (

              <TicketController
                value={quantity}
                onChange={value => this.handleTicketsChange(value)}
              />
              {this.props.onClick && (
                <CTAButton
                  className="ctabtn blue ticket"
                  label={this.buttonLabel}
                  onClick={this.handleSubmit}
                />
              )}

          )}
          {type !== 'TICKET' &&
            this.props.onClick && (

                <CTAButton
                  className="ctabtn blue"
                  label={tier.button || this.buttonLabel}
                  onClick={this.handleSubmit}
                />

            )}

    );
  }
}

export default withIntl(Tier);
<|endoftext|>