Raw Source
artbit / XGroups

// ==UserScript==
// @name         XGroups
// @description  Allows adding x.com users to groups, storing in localStorage, and displaying group tags below avatars
// @namespace    https://github.com/artbit/xgroups
// @author       Djordje Ungar
// @copyright    2025+ Djordje Ungar
// @license      MIT
// @version      1.0.87
// @source       https://github.com/artbit/xgroups
// @homepageURL  https://github.com/artbit/xgroups
// @supportURL   https://github.com/artbit/xgroups/issues
// @updateURL    https://raw.githubusercontent.com/ArtBIT/xgroups/refs/heads/main/dist/xgroups.meta.js
// @downloadURL  https://raw.githubusercontent.com/ArtBIT/xgroups/refs/heads/main/dist/xgroups.userscript.js
// @match        https://x.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=x.com
// @tag          productivity
// @run-at       document-idle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// ==/UserScript==

// ==OpenUserJS==
// @author       artbit
// ==/OpenUserJS==

/*jshint esversion: 9 */
(() => {
  let c = {
      tweetSelector: 'article[data-testid="tweet"]',
      usernameSelector: '[data-testid="User-Name"] :nth-child(2) a[href^="/"][role="link"] span',
      avatarSelector: 'div[data-testid="Tweet-User-Avatar"]'
    },
    l = (...e) => e.filter(Boolean).join(" "),
    p = {
      groups: '<svg viewBox="0 0 24 24" aria-label="XGroups" role="img"><g><path d="M7.501 19.917L7.471 21H.472l.029-1.027c.184-6.618 3.736-8.977 7-8.977.963 0 1.95.212 2.87.672-.444.478-.851 1.03-1.212 1.656-.507-.204-1.054-.329-1.658-.329-2.767 0-4.57 2.223-4.938 6.004H7.56c-.023.302-.05.599-.059.917zm8.999-8.921c-3.264 0-6.816 2.358-7 8.977L9.471 21h4.528v-2h-2.438c.367-3.781 2.17-6.004 4.938-6.004 1.089 0 2.022.356 2.784 1.004h2.632c-1.376-2.136-3.446-3.004-5.415-3.004zm0-.996c-.799 0-1.527-.279-2.116-.73C13.548 8.63 13 7.632 13 6.5 13 4.57 14.567 3 16.5 3S20 4.57 20 6.5c0 1.132-.548 2.13-1.384 2.77-.589.451-1.317.73-2.116.73zM15 6.5c0 .827.673 1.5 1.5 1.5S18 7.327 18 6.5 17.327 5 16.5 5 15 5.673 15 6.5zm-11 0C4 4.57 5.567 3 7.5 3S11 4.57 11 6.5 9.433 10 7.5 10 4 8.43 4 6.5zm2 0C6 7.327 6.673 8 7.5 8S9 7.327 9 6.5 8.327 5 7.5 5 6 5.673 6 6.5zM21 21h3v-2h-3v-3h-2v3h-3v2h3v3h2v-3z"></path></g></svg>'
    },
    d = (() => {
      let o = "https://api.github.com/gists",
        e = () => GM_getValue("xgroups_gist_token", ""),
        a = () => ({
          Accept: "application/vnd.github.v3+json",
          Authorization: "token " + e(),
          "Content-Type": "application/json"
        });
      return {
        createGist: async e => new Promise((r, n) => {
          GM_xmlhttpRequest({
            url: o,
            method: "POST",
            headers: a(),
            nocache: !0,
            data: JSON.stringify({
              description: "X.com User Group Tagger Data",
              public: !1,
              files: {
                "xgroups_data.json": {
                  content: JSON.stringify(e, null, 2)
                }
              }
            }),
            onload: e => {
              let t;
              try {
                t = JSON.parse(e.responseText)
              }
              catch (e) {
                n(e)
              }
              200 !== e.status && n(new Error("Failed to create Gist"));
              e = t.id;
              return GM_setValue("xgroups_gist_id", e), r(e)
            }
          })
        }),
        readGist: async e => new Promise((r, n) => {
          GM_xmlhttpRequest({
            url: o + "/" + e,
            headers: a(),
            nocache: !0,
            onload: e => {
              let t;
              try {
                t = JSON.parse(e.responseText)
              }
              catch (e) {
                n(e)
              }
              e = t.files;
              r(JSON.parse(e["xgroups_data.json"].content))
            }
          })
        }),
        updateGist: async (n, e) => new Promise((t, r) => {
          GM_xmlhttpRequest({
            url: o + "/" + n,
            method: "PATCH",
            headers: a(),
            nocache: !0,
            data: JSON.stringify({
              files: {
                "xgroups_data.json": {
                  content: JSON.stringify(e, null, 2)
                }
              }
            }),
            onload: e => {
              try {
                JSON.parse(e.responseText)
              }
              catch (e) {
                r(e)
              }
              200 !== e.status && r(new Error("Failed to update Gist")), t(n)
            }
          })
        }),
        getToken: e,
        getGistId: () => GM_getValue("xgroups_gist_id", "")
      }
    })();
  class u {
    constructor(e, t) {
      this.props = e || {}, this.store = t, this.state = {}, this.element = null, this.unsubscribe = null
    }
    setState(e) {
      this.state = {
        ...this.state,
        ...e
      }, this.update()
    }
    setProps(e) {
      this.props = {
        ...this.props,
        ...e
      }, this.update()
    }
    render() {
      return g({
        tag: "div",
        textContent: "Base Component"
      })
    }
    mount(e) {
      this.element = this.render(), e.appendChild(this.element), this.store && (this.unsubscribe = this.store.subscribe(() => this.update()))
    }
    update() {
      var e;
      this.element && (e = this.render(), h(this.element, e))
    }
    unmount() {
      this.element && (this.element.remove(), this.element = null), this.unsubscribe && this.unsubscribe()
    }
  }
  let g = ({
      tag: e = "div",
      children: t = [],
      style: r = {},
      on: n = {},
      ...o
    }) => {
      let a = document.createElement(e);
      return Object.entries(o).forEach(([e, t]) => {
        "className" === e ? a.className = t : "textContent" === e ? a.textContent = t : "innerHTML" === e ? a.innerHTML = t : a.setAttribute(e, t)
      }), Object.entries(r).forEach(([e, t]) => {
        a.style[e] = t
      }), Object.entries(n).forEach(([e, t]) => {
        a.addEventListener(e, t)
      }), t.forEach(e => {
        e && ("string" == typeof e ? a.appendChild(document.createTextNode(e)) : e instanceof HTMLElement ? a.appendChild(e) : e instanceof u ? e.mount(a) : a.appendChild(g(e)))
      }), a
    },
    m = e => {
      setTimeout(e, 1)
    },
    _ = (e = {}) => {
      GM_notification({
        title: e.title || "XGroups",
        text: e.text || "This is the notification message.",
        ...e
      }), GM_log("Notification: " + e.text)
    },
    h = (n, e) => {
      if (n || e) {
        if (!n) return e;
        if (e)
          if (n.tagName !== e.tagName || 0 === n.childNodes.length && 0 === e.childNodes.length) n.replaceWith(e);
          else {
            if (0 < n.childNodes.length || 0 < e.childNodes.length) {
              var o = Array.from(n.childNodes),
                a = Array.from(e.childNodes);
              for (let e = 0; e < Math.max(o.length, a.length); e++) {
                var s = h(o[e], a[e]);
                s && n.appendChild(s)
              }
            }
            var i = Array.from(n.attributes),
              e = Array.from(e.attributes);
            let t = Object.fromEntries(i.map(e => [e.name, e.value])),
              r = Object.fromEntries(e.map(e => [e.name, e.value]));
            Object.keys(t).forEach(e => {
              e in r || n.removeAttribute(e)
            }), Object.keys(r).forEach(e => {
              t[e] !== r[e] && n.setAttribute(e, r[e])
            })
          }
        else n.remove()
      }
    },
    e = i => {
      let t = "xgroups_groups",
        r = "xgroups_user_groups",
        n = "xgroups_data_timestamp",
        o = {
          groups: null,
          userGroups: null,
          timestamp: null
        },
        c = () => {
          try {
            var e = i.getState().groups;
            JSON.stringify(e) !== JSON.stringify(o.groups) && (GM_setValue(t, JSON.stringify(e)), s(), o.groups = e)
          }
          catch (e) {
            console.error("Failed to save groups:", e), _({
              text: "Unable to save group data."
            })
          }
        },
        a = e => (e || "").replace("@", "").toLowerCase(),
        s = () => {
          var e = Date.now();
          GM_setValue(n, e.toString())
        };
      let l = () => ({
        groups: i.getState().groups,
        userGroups: Object.fromEntries(Object.entries(i.getState().userGroups).map(([e, t]) => [e, Array.from(t)])),
        timestamp: parseInt(GM_getValue(n, "0"), 10)
      });
      let p = () => {
        var e = Object.fromEntries(Object.entries(i.getState().userGroups).map(([e, t]) => [e, Array.from(t)]));
        try {
          JSON.stringify(e) !== JSON.stringify(o.userGroups) && (GM_setValue(r, JSON.stringify(e)), s(), o.userGroups = e)
        }
        catch (e) {
          console.error("Failed to save user groups:", e), _({
            text: "Unable to save user group data."
          })
        }
      };
      i.setState({
        groups: (() => {
          try {
            return (JSON.parse(GM_getValue(t, "[]")) || []).map(e => ({
              name: e.name,
              bgColor: e.bgColor || "#777",
              fgColor: e.fgColor || "#fff",
              description: e.description || ""
            }))
          }
          catch (e) {
            return console.error("Failed to load groups:", e), []
          }
        })(),
        userGroups: (() => {
          try {
            var e = JSON.parse(GM_getValue(r, "{}")) || {};
            return Object.fromEntries(Object.entries(e).map(([e, t]) => [e, new Set(t)]))
          }
          catch (e) {
            return console.error("Failed to load user groups:", e), {}
          }
        })()
      });
      let u = () => i.getState().groups,
        g = t => i.getState().groups.find(e => e.name === t);
      return {
        getGroups: u,
        getGroup: g,
        addGroup: (e, t = "", r = "#777", n = "#fff") => {
          e = {
            name: e,
            description: t,
            bgColor: r,
            fgColor: n
          };
          return i.setState({
            groups: [...i.getState().groups, e]
          }), c(), e
        },
        removeGroup: t => {
          let r = i.getState().userGroups;
          Object.keys(r).forEach(e => {
            r[e].delete(t), 0 === r[e].size && delete r[e]
          }), i.setState({
            groups: i.getState().groups.filter(e => e.name !== t),
            userGroups: {
              ...r
            }
          }), c(), p()
        },
        updateGroup: (r, n, e, t, o) => {
          if (r !== n && g(n)) _({
            text: "Group with this name already exists."
          });
          else {
            var a = u(),
              s = a.findIndex(e => e.name === r);
            if (-1 !== s) {
              e = {
                ...a[s],
                name: n,
                description: e,
                bgColor: t,
                fgColor: o
              };
              if (a[s] = e, i.setState({
                  groups: a
                }), c(), r !== n) {
                let t = i.getState().userGroups;
                Object.keys(t).forEach(e => {
                  t[e].has(r) && (t[e].delete(r), t[e].add(n))
                }), i.setState({
                  userGroups: {
                    ...t
                  }
                }), p()
              }
            }
          }
        },
        getUserGroups: e => Array.from(i.getState().userGroups[a(e)] || []),
        getUserLink: e => "https://x.com/" + a(e),
        getGroupUsers: t => Object.keys(i.getState().userGroups).filter(e => i.getState().userGroups[e].has(t)),
        addUserToGroup: (e, t) => {
          e = a(e);
          var r = i.getState().userGroups;
          r[e] = r[e] || new Set, r[e].add(t), i.setState({
            userGroups: {
              ...r
            }
          }), p()
        },
        removeUserFromGroup: (e, t) => {
          e = a(e);
          var r = i.getState().userGroups;
          r[e] && (r[e].delete(t), 0 === r[e].size && delete r[e], i.setState({
            userGroups: {
              ...r
            }
          }), p())
        },
        normalizeUsername: a,
        getLocalData: l,
        importData: r => {
          try {
            let t = JSON.parse(r);
            if (!t.groups || !t.userGroups) throw new Error("Invalid JSON format");
            let e = i.getState().groups,
              n = i.getState().userGroups;
            var o = t.groups.filter(t => !e.some(e => e.name === t.name));
            return i.setState({
              groups: [...e, ...o.map(e => ({
                name: e.name,
                bgColor: e.bgColor || "#777",
                fgColor: e.fgColor || "#fff",
                description: e.description || ""
              }))]
            }), Object.keys(t.userGroups).forEach(e => {
              let r = a(e);
              n[r] || (n[r] = new Set), t.userGroups[e].forEach(t => {
                i.getState().groups.some(e => e.name === t) && n[r].add(t)
              }), 0 === n[r].size && delete n[r]
            }), i.setState({
              userGroups: {
                ...n
              }
            }), s(), p(), c(), !0
          }
          catch (e) {
            return console.error("Failed to import data:", e), _({
              text: "Invalid JSON file or format."
            }), !1
          }
        },
        syncWithGist: async () => {
          if (d.getToken() && d.getGistId()) try {
            return await (async (e, r = 3, n = 1e3) => {
              for (let t = 0; t < r; t++) try {
                return await e()
              }
              catch (e) {
                if (t === r - 1) throw e;
                await new Promise(e => setTimeout(e, n * 2 ** t))
              }
            })(async () => {
              var e = l(),
                t = parseInt(e.timestamp || "0", 10),
                r = await d.readGist(d.getGistId()),
                n = parseInt(r.timestamp || "0", 10);
              if (t < n) i.setState({
                groups: r.groups,
                userGroups: Object.fromEntries(Object.entries(r.userGroups).map(([e, t]) => [e, new Set(t)]))
              }), c(), p();
              else if (n < t) return d.updateGist(d.getGistId(), e)
            })
          }
          catch (e) {
            console.error("Failed to sync with Gist:", e)
          }
        },
        createGist: async () => {
          if (d.getToken()) try {
            var e = l(),
              t = await d.createGist(e);
            return GM_setValue("xgroups_gist_id", t), id
          }
          catch (e) {
            console.error("Failed to create Gist:", e), _({
              text: "Failed to create Gist."
            })
          }
          else _({
            text: "Please set your Gist token in localStorage."
          })
        }
      }
    },
    x = {
      xgroups: "_xgroups_cwhct_10",
      "form-element": "_form-element_cwhct_10",
      "form-input": "_form-input_cwhct_14",
      "xgroups-svg-btn": "_xgroups-svg-btn_cwhct_26",
      "group-tags": "_group-tags_cwhct_33",
      "group-tag": "_group-tag_cwhct_33",
      "group-tags-btns": "_group-tags-btns_cwhct_46",
      "group-item": "_group-item_cwhct_61",
      "modal-overlay": "_modal-overlay_cwhct_73",
      "modal-window": "_modal-window_cwhct_85",
      "modal-header": "_modal-header_cwhct_95",
      "form-title": "_form-title_cwhct_102",
      "form-subtitle": "_form-subtitle_cwhct_106",
      "modal-content": "_modal-content_cwhct_111",
      active: "_active_cwhct_120",
      "group-btn": "_group-btn_cwhct_132",
      "text-btn": "_text-btn_cwhct_145",
      "form-footer": "_form-footer_cwhct_177",
      "label-bg": "_label-bg_cwhct_235",
      placeholder: "_placeholder_cwhct_252",
      "loading-spinner": "_loading-spinner_cwhct_299",
      "loading-spinner-inner": "_loading-spinner-inner_cwhct_308",
      "loading-spinner-cutout": "_loading-spinner-cutout_cwhct_317",
      hidden: "_hidden_cwhct_335"
    };
  class b extends u {
    render() {
      let {
        options: e,
        ...t
      } = this.props;
      return g({
        tag: "select",
        className: x["form-input"],
        children: e.map(({
          label: e,
          value: t
        }) => ({
          tag: "option",
          value: t,
          textContent: e
        })),
        ...t
      })
    }
  }
  class f extends u {
    render() {
      let {
        type: e,
        label: t,
        name: r,
        value: n = "",
        ...o
      } = this.props;
      return g({
        className: x["form-element"],
        children: [{
          tag: "input",
          type: e,
          name: r,
          value: n,
          className: x["form-input"],
          ...o
        }, t && {
          tag: "div",
          className: x["label-bg"],
          textContent: t
        }, t && {
          tag: "label",
          className: x.placeholder,
          textContent: t,
          for: r
        }]
      })
    }
  }
  class w extends u {
    render() {
      let {
        svg: e,
        label: t = "",
        ariaLabel: r,
        ...n
      } = this.props;
      return g({
        ...n,
        tag: "div",
        role: "button",
        className: x["xgroups-svg-btn"],
        "aria-label": r || t,
        children: [{
          tag: "div",
          innerHTML: e
        }, t && {
          tag: "span",
          textContent: t
        }]
      })
    }
  }
  class y extends u {
    render() {
      var e = this.store.getState().loading;
      return g({
        ...this.props,
        tag: "div",
        className: l(x["loading-spinner"], !e && x.hidden),
        children: [{
          tag: "div",
          className: x["loading-spinner-inner"]
        }, {
          tag: "div",
          className: x["loading-spinner-cutout"]
        }]
      })
    }
  }
  class v extends u {
    render() {
      let {
        title: e,
        subtitle: t,
        template: r,
        onClose: n,
        active: o
      } = this.props;
      return g({
        tag: "div",
        className: l(x.xgroups, x["modal-overlay"], o && x.active),
        on: {
          keydown: e => "Escape" === e.key && n()
        },
        children: [{
          tag: "div",
          className: l(x["modal-window"]),
          children: [{
            tag: "div",
            className: x.xgroups,
            role: "dialog",
            "aria-modal": "true",
            style: {
              position: "relative"
            },
            children: [{
              tag: "div",
              className: x["modal-header"],
              children: [{
                tag: "h2",
                className: x["form-title"],
                textContent: e
              }, t && "string" == typeof t && {
                tag: "h3",
                className: x["form-subtitle"],
                textContent: t
              }, t && t instanceof HTMLElement && {
                tag: "div",
                className: x["form-subtitle"],
                children: [t]
              }]
            }, {
              tag: "div",
              className: x["modal-content"],
              children: [null == r ? void 0 : r(this.props, this.store)]
            }, {
              tag: "button",
              textContent: "✖️",
              "aria-label": "Close modal",
              style: {
                position: "absolute",
                top: "10px",
                right: "10px"
              },
              on: {
                click: n
              }
            }]
          }]
        }]
      })
    }
    mount(e) {
      super.mount(e), this.props.active || m(() => {
        this.setProps({
          active: !0
        })
      }), this.element.focus()
    }
    unmount() {
      this.element && (this.setProps({
        active: !1
      }), setTimeout(super.unmount.bind(this), 200))
    }
  }
  let t = (s, n) => {
    let i = (n => {
        let o = new Map;
        let a = () => {
          var e = n.getState().modalsStack;
          e.length && e.pop().unmount(), n.setState({
            modalsStack: e
          })
        };
        return n.subscribe(t => {
          t = t.modalsStack;
          if (t.length) {
            let e = t[t.length - 1];
            e.props.active || m(() => {
              e.setProps({
                active: !0
              })
            })
          }
        }), {
          register: (e, t) => {
            o.set(e, t)
          },
          open: (e, t = {}) => {
            var r = o.get(e);
            if (!r) throw new Error(`Modal ${e} not registered`);
            t = new v({
              ...t,
              modalName: e,
              template: r,
              onClose: () => a()
            }, n), e = n.getState().modalsStack || [];
            e.forEach(e => {
              e.setProps({
                active: !1
              })
            }), e.push(t), t.mount(document.body), n.setState({
              modalsStack: e
            })
          },
          close: a
        }
      })(n),
      t = !0;
    var e = g({
      tag: "style",
      id: "xgroups-styles",
      textContent: ':root {\n  --xgroups-background-primary: #0f1419;\n  --xgroups-background-secondary: #30383f;\n  --xgroups-typography-primary: #eee;\n  --xgroups-typography-secondary: #ccc;\n  --xgroups-typography-inverted: #000;\n  --xgroups-ui-primary: #eff3f4;\n  --xgroups-border-radius: 24px;\n}\n._xgroups_cwhct_10 ._form-element_cwhct_10 {\n  position: relative;\n  margin-top: 28px;\n}\n._xgroups_cwhct_10 ._form-input_cwhct_14 {\n  background: var(--xgroups-background-secondary);\n  color: var(--xgroups-typography-primary);\n  border: 0;\n  border-radius: var(--xgroups-border-radius);\n  padding: 4px 20px;\n  min-height: 50px;\n  width: 100%;\n}\n._xgroups_cwhct_10 ._group-circle-btn_cwhct_23:hover {\n  background: var(--xgroups-background-secondary);\n}\n._xgroups-svg-btn_cwhct_26 {\n  fill: var(--xgroups-typography-secondary);\n  transition: fill 200ms;\n}\n._xgroups-svg-btn_cwhct_26:hover {\n  fill: var(--xgroups-typography-primary);\n}\n._xgroups_cwhct_10._group-tags_cwhct_33 {\n  margin-top: 5px;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n._xgroups_cwhct_10 ._group-tag_cwhct_33 {\n  border-radius: 4px;\n  padding: 2px 6px;\n  margin-bottom: 2px;\n  font-size: 16px;\n  cursor: pointer;\n}\n._xgroups_cwhct_10 ._group-tags-btns_cwhct_46 {\n  gap: 5px;\n  /* add gradient fallof from the top */\n  background: linear-gradient(\n    to bottom,\n    var(--xgroups-background-primary) 0%,\n    rgba(0, 0, 0, 0) 100%\n  );\n  padding-top: 5px;\n  margin-top: 5px;\n  visibility: hidden;\n}\narticle[data-testid="tweet"]:hover ._group-tags-btns_cwhct_46 {\n  visibility: visible;\n}\n._xgroups_cwhct_10 ._group-item_cwhct_61 {\n  display: flex;\n  gap: 8px;\n  justify-content: space-between;\n  padding: 0 8px;\n}\n._xgroups_cwhct_10 ._group-item_cwhct_61:hover {\n  background: var(--xgroups-background-secondary);\n}\n\n/* MODALS */\n\n._xgroups_cwhct_10._modal-overlay_cwhct_73 {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background: rgba(0, 0, 0, 0.8);\n  z-index: 1002;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n._xgroups_cwhct_10 ._modal-window_cwhct_85 {\n  color: var(--xgroups-typography-primary);\n  background: var(--xgroups-background-primary);\n  border: none;\n  padding: 15px;\n  max-width: 400px;\n  width: 100%;\n  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);\n  border-radius: var(--xgroups-border-radius);\n}\n._xgroups_cwhct_10 ._modal-header_cwhct_95 {\n  display: flex;\n  justify-content: space-between;\n  align-items: flex-start;\n  margin-bottom: 10px;\n  flex-direction: column;\n}\n._xgroups_cwhct_10 ._modal-header_cwhct_95 ._form-title_cwhct_102 {\n  font-size: 20px;\n  margin: 0;\n}\n._xgroups_cwhct_10 ._modal-header_cwhct_95 ._form-subtitle_cwhct_106 {\n  font-size: 14px;\n  color: #aaa;\n  margin: 0;\n}\n._xgroups_cwhct_10 ._modal-content_cwhct_111 {\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n}\n._xgroups_cwhct_10._modal-overlay_cwhct_73 {\n  opacity: 0;\n  transition: opacity 200ms ease-in-out;\n}\n._xgroups_cwhct_10._modal-overlay_cwhct_73._active_cwhct_120 {\n  opacity: 1;\n}\n._xgroups_cwhct_10._modal-overlay_cwhct_73._active_cwhct_120 ._modal-window_cwhct_85 {\n  transform: scale(1);\n}\n._xgroups_cwhct_10 ._modal-window_cwhct_85 {\n  transform: scale(0.8);\n  transition: transform 200ms ease-in-out;\n}\n\n._xgroups_cwhct_10 input[type="submit"],\n._xgroups_cwhct_10 ._group-btn_cwhct_132 {\n  background-color: var(--xgroups-ui-primary);\n  color: var(--xgroups-typography-inverted);\n  border-radius: 12px;\n  border: 0;\n  box-sizing: border-box;\n  cursor: pointer;\n  font-size: 14px;\n  margin: 4px;\n  padding: 8px;\n  outline: 0;\n  text-align: center;\n}\n._xgroups_cwhct_10 ._text-btn_cwhct_145 {\n  background-color: none;\n  color: var(--xgroups-typography-secondary);\n  border-radius: 12px;\n  border: 0;\n  box-sizing: border-box;\n  cursor: pointer;\n  font-size: 14px;\n  margin: 4px;\n  padding: 4px;\n  outline: 0;\n  text-align: center;\n}\n._xgroups_cwhct_10 ._text-btn_cwhct_145:hover {\n  color: var(--xgroups-typography-primary);\n  text-decoration: underline;\n}\n._xgroups_cwhct_10 form {\n  padding: 0px;\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  background-color: var(--xgroups-background-primary);\n  border-radius: var(--xgroups-border-radius);\n  box-sizing: border-box;\n}\n._xgroups_cwhct_10 ._form-title_cwhct_102 {\n  color: var(--xgroups-typography-primary);\n  font-family: sans-serif;\n  font-size: 24px;\n  font-weight: 600;\n}\n._xgroups_cwhct_10 ._form-footer_cwhct_177 {\n  color: var(--xgroups-typography-secondary);\n  font-family: sans-serif;\n  font-size: 14px;\n  font-weight: 400;\n  margin-top: 10px;\n}\n._xgroups_cwhct_10 select {\n  padding: 12px;\n  border-radius: var(--xgroups-border-radius);\n  background: var(--xgroups-background-secondary);\n}\n._xgroups_cwhct_10 ._form-subtitle_cwhct_106 {\n  color: var(--xgroups-typography-primary);\n  font-family: sans-serif;\n  font-size: 16px;\n  font-weight: 600;\n  margin-top: 10px;\n}\n._xgroups_cwhct_10 ._form-element_cwhct_10 {\n  min-height: 50px;\n  height: auto;\n  position: relative;\n  width: 100%;\n  margin-top: 28px;\n}\n._xgroups_cwhct_10 ._divider_cwhct_203 {\n  width: 100%;\n  height: 1px;\n  background-color: var(--xgroups-background-secondary);\n  margin: 20px 0;\n}\n._xgroups_cwhct_10 ._form-input_cwhct_14,\n._xgroups_cwhct_10 input {\n  color: var(--xgroups-typography-primary);\n  background-color: var(--xgroups-background-secondary);\n  border-radius: var(--xgroups-border-radius);\n  border: 0;\n  box-sizing: border-box;\n  font-size: 18px;\n  min-height: 50px;\n  height: 100%;\n  outline: 0;\n  padding: 4px 20px;\n  width: 100%;\n}\n._xgroups_cwhct_10 input[type="color"] {\n  -webkit-appearance: none;\n  width: 100%;\n  height: 50px;\n}\n._xgroups_cwhct_10 input[type="color"],\n._xgroups_cwhct_10 input[type="color"]::-webkit-color-swatch,\n._xgroups_cwhct_10 input[type="color"]::-webkit-color-swatch-wrapper {\n  border: none;\n  border-radius: var(--xgroups-border-radius);\n  padding: 0;\n}\n._xgroups_cwhct_10 ._label-bg_cwhct_235 {\n  background-color: var(--xgroups-background-primary);\n  border-radius: 10px;\n  padding: 4px 10px;\n  height: 20px;\n  position: absolute;\n  top: -28px;\n  left: 20px;\n  transform: translateY(0);\n  transition: transform 200ms;\n  color: transparent;\n}\n._xgroups_cwhct_10 ._form-input_cwhct_14._focus_cwhct_247 ~ ._label-bg_cwhct_235,\n._xgroups_cwhct_10 input:focus ~ ._label-bg_cwhct_235,\n._xgroups_cwhct_10 input:not(:placeholder-shown) ~ ._label-bg_cwhct_235 {\n  transform: translateY(14px);\n}\n._xgroups_cwhct_10 ._placeholder_cwhct_252 {\n  color: var(--xgroups-typography-secondary);\n  position: absolute;\n  top: 20px;\n  left: 20px;\n  pointer-events: none;\n  transform-origin: 0 50%;\n  transition:\n    transform 200ms,\n    color 200ms;\n  transform: scale(1.2);\n}\n._xgroups_cwhct_10 ._label-bg_cwhct_235,\n._xgroups_cwhct_10 ._placeholder_cwhct_252 {\n  font-family: sans-serif;\n  font-size: 14px;\n  line-height: 14px;\n}\n._xgroups_cwhct_10 ._form-input_cwhct_14._focus_cwhct_247 ~ ._placeholder_cwhct_252,\n._xgroups_cwhct_10 ._form-input_cwhct_14 ._placeholder-shown_cwhct_271,\n._xgroups_cwhct_10 input:focus ~ ._placeholder_cwhct_252,\n._xgroups_cwhct_10 input:not(:placeholder-shown) ~ ._placeholder_cwhct_252 {\n  transform: translateY(-34px) translateX(9px) scale(1);\n}\n._xgroups_cwhct_10 input:not(:placeholder-shown) ~ ._placeholder_cwhct_252 {\n  color: var(--xgroups-typography-secondary);\n}\n._xgroups_cwhct_10 input:focus ~ ._placeholder_cwhct_252 {\n  color: var(--xgroups-typography-tertiary);\n}\n._xgroups_cwhct_10 ._submit_cwhct_282 {\n  background-color: var(--xgroups-ui-primary);\n  color: var(--xgroups-typography-primary);\n  border-radius: 12px;\n  border: 0;\n  box-sizing: border-box;\n  cursor: pointer;\n  font-size: 18px;\n  height: 50px;\n  margin-top: 38px;\n  outline: 0;\n  text-align: center;\n  width: 100%;\n}\n._xgroups_cwhct_10 ._submit_cwhct_282:active {\n  background-color: var(--xgroups-ui-secondary);\n}\n._xgroups_cwhct_10 ._loading-spinner_cwhct_299 {\n  position: relative;\n  display: inline-block;\n  top: 6px;\n  width: 24px;\n  height: 24px;\n  margin: 0 auto;\n  animation: _spin_cwhct_1 1s linear infinite;\n}\n._xgroups_cwhct_10 ._loading-spinner-inner_cwhct_308 {\n  position: absolute;\n  width: 50%;\n  height: 50%;\n  top: 0;\n  left: 0;\n  background-color: var(--xgroups-ui-primary);\n  border-radius: 100% 0 0 0;\n}\n._xgroups_cwhct_10 ._loading-spinner-cutout_cwhct_317 {\n  width: 50%;\n  height: 50%;\n  background-color: var(--xgroups-background-primary);\n  position: absolute;\n  top: 25%;\n  left: 25%;\n  border-radius: 50%;\n  pointer-events: none;\n}\n@keyframes _spin_cwhct_1 {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n._xgroups_cwhct_10 ._hidden_cwhct_335 {\n  display: none;\n}\n'
    });
    document.head.appendChild(e), i.register("groupManager", (e, t) => g({
      tag: "div",
      className: x.xgroups,
      children: [{
        style: {
          display: "flex",
          alignItems: "flex-start"
        },
        children: [{
          tag: "button",
          title: "Import groups from JSON",
          textContent: "Import...",
          className: x["text-btn"],
          on: {
            click: () => {
              i.open("importData", {
                title: "Import Data",
                subtitle: "Paste your JSON data"
              })
            }
          }
        }, {
          tag: "button",
          title: "Export groups to JSON",
          textContent: "Export",
          className: x["text-btn"],
          on: {
            click: () => {
              var e = JSON.stringify(s.getLocalData()),
                e = new Blob([e], {
                  type: "application/json"
                }),
                e = URL.createObjectURL(e),
                t = g({
                  tag: "a",
                  href: e,
                  download: "xgroups.json",
                  style: {
                    display: "none"
                  }
                });
              document.body.appendChild(t), t.click(), document.body.removeChild(t), URL.revokeObjectURL(e)
            }
          }
        }, {
          tag: "button",
          title: "Sync with Gist",
          textContent: "Sync...",
          className: x["text-btn"],
          on: {
            click: () => {
              i.open("gistSettings", {
                title: "Gist Settings"
              })
            }
          }
        }]
      }, {
        tag: "div",
        style: {
          overflowY: "auto",
          maxHeight: "300px"
        },
        children: [...t.getState().groups.map(e => ({
          tag: "div",
          className: x["group-item"],
          children: [{
            tag: "button",
            textContent: `${e.name} (${s.getGroupUsers(e.name).length})`,
            title: e.description,
            on: {
              click: () => {
                i.open("groupUsers", {
                  groupName: e.name,
                  title: "Users in Group: " + e.name,
                  subtitle: g({
                    tag: "button",
                    textContent: "Edit group...",
                    className: x["text-btn"],
                    on: {
                      click: () => i.open("editGroup", {
                        group: e,
                        title: "Edit Group"
                      })
                    }
                  })
                })
              }
            }
          }, {
            tag: "div",
            style: {
              display: "inline-flex",
              gap: "5px"
            },
            children: [{
              tag: "button",
              textContent: "Edit",
              className: x["text-btn"],
              on: {
                click: () => {
                  i.open("editGroup", {
                    group: e,
                    title: "Edit Group"
                  })
                }
              }
            }, {
              tag: "button",
              textContent: "Remove",
              className: x["text-btn"],
              title: "Remove group",
              on: {
                click: () => {
                  0 < s.getGroupUsers(e.name).length && !confirm(`Are you sure you want to remove the group "${e.name}"?`) || s.removeGroup(e.name)
                }
              }
            }]
          }]
        }))]
      }, {
        tag: "button",
        textContent: "Add Group",
        className: x["group-btn"],
        on: {
          click: () => {
            i.open("addGroup", {
              title: "Add Group"
            })
          }
        }
      }]
    })), i.register("gistSettings", (e, t) => g({
      tag: "form",
      className: x.xgroups,
      on: {
        submit: async e => {
          e.preventDefault();
          var t = e.target.elements.token.value,
            r = e.target.elements.gistId.value;
          if ("cancel" === e.submitter.name) i.close();
          else if (t) {
            if (GM_setValue("xgroups_gist_token", t), r) GM_setValue("xgroups_gist_id", r);
            else try {
              var n = await s.createGist();
              n && _({
                text: "Gist created: https://gist.github.com/" + n,
                url: "https://gist.github.com/" + n
              })
            }
            catch (e) {
              return console.error("Failed to create Gist:", e), void _({
                text: "Failed to create Gist. Check your token."
              })
            }
            await s.syncWithGist(), i.close()
          }
          else _({
            text: "Please enter a GitHub Personal Access Token."
          })
        }
      },
      children: [new f({
        type: "text",
        label: "GitHub Personal Access Token",
        name: "token",
        value: GM_getValue("xgroups_gist_token", "")
      }, t).render(), new f({
        type: "text",
        label: "Gist ID (leave blank to create new)",
        name: "gistId",
        value: GM_getValue("xgroups_gist_id", "")
      }, t).render(), {
        tag: "p",
        children: [{
          tag: "a",
          textContent: "Generate new Personal Access Token?",
          href: "https://github.com/settings/personal-access-tokens/new",
          target: "_blank",
          style: {
            color: "var(--xgroups-ui-primary)",
            textDecoration: "underline"
          }
        }, {
          tag: "span",
          textContent: " (only allow 'gist' scope)"
        }]
      }, g({
        className: x["form-footer"],
        children: [{
          tag: "button",
          type: "submit",
          name: "cancel",
          className: x["group-btn"],
          textContent: "Cancel"
        }, {
          tag: "button",
          type: "submit",
          name: "save",
          className: x["group-btn"],
          textContent: "Save"
        }, {
          tag: "button",
          type: "button",
          name: "sync",
          className: x["group-btn"],
          textContent: "Sync Now",
          on: {
            click: async () => {
              t.setState({
                loading: !0
              });
              var e = await s.syncWithGist();
              t.setState({
                loading: !1
              }), e && (_({
                text: "Gist updated: https://gist.github.com/" + e,
                url: "https://gist.github.com/" + e
              }), i.close())
            }
          }
        }, {
          tag: "span",
          children: [new y({}, t)]
        }]
      })]
    })), i.register("importData", (e, t) => g({
      tag: "form",
      className: x.xgroups,
      on: {
        submit: e => {
          e.preventDefault();
          e = e.target.elements.jsonData.value;
          s.importData(e) ? i.close() : _({
            text: "Invalid JSON data."
          })
        }
      },
      children: [{
        tag: "textarea",
        className: x["form-input"],
        placeholder: "Paste your JSON data here",
        rows: 20,
        on: {
          input: e => {
            e = e.target.value;
            s.importData(e) && i.close()
          }
        }
      }, {
        tag: "button",
        textContent: "Import",
        className: x["group-btn"],
        type: "submit"
      }]
    })), i.register("editGroup", ({
      group: a
    }, e) => g({
      tag: "form",
      className: x.xgroups,
      on: {
        submit: e => {
          var t, r, n, o;
          e.preventDefault(), "cancel" === e.submitter.name ? i.close() : (t = e.target.elements.name.value, r = e.target.elements.description.value, n = e.target.elements.bgColor.value, e = e.target.elements.fgColor.value, t ? (o = a.name, s.updateGroup(o, t, r, n, e), i.close()) : _({
            text: "Please enter a group name."
          }))
        }
      },
      children: [new f({
        type: "text",
        label: "Group Name",
        name: "name",
        value: a.name
      }, e).render(), new f({
        type: "text",
        label: "Description",
        name: "description",
        value: a.description
      }, e).render(), new f({
        type: "color",
        label: "Background Color",
        name: "bgColor",
        value: a.bgColor
      }, e).render(), new f({
        type: "color",
        label: "Text Color",
        name: "fgColor",
        value: a.fgColor
      }, e).render(), g({
        className: x["form-footer"],
        children: [{
          tag: "button",
          type: "submit",
          name: "cancel",
          className: x["group-btn"],
          textContent: "Cancel"
        }, {
          tag: "button",
          type: "submit",
          name: "save",
          className: x["group-btn"],
          textContent: "Save Changes"
        }]
      })]
    })), i.register("addGroup", (e, t) => g({
      tag: "form",
      className: x.xgroups,
      on: {
        submit: e => {
          e.preventDefault();
          var t = e.target.elements.name.value,
            r = e.target.elements.description.value,
            n = e.target.elements.bgColor.value,
            e = e.target.elements.fgColor.value;
          t ? (s.addGroup(t, r, n, e), i.close()) : _({
            text: "Please enter a group name."
          })
        }
      },
      children: [new f({
        type: "text",
        label: "Group Name",
        name: "name"
      }, t).render(), new f({
        type: "text",
        label: "Description",
        name: "description"
      }, t).render(), new f({
        type: "color",
        label: "Background Color",
        name: "bgColor",
        value: "#777777"
      }, t).render(), new f({
        type: "color",
        label: "Text Color",
        name: "fgColor",
        value: "#ffffff"
      }, t).render(), g({
        className: x["form-footer"],
        children: [{
          tag: "button",
          type: "submit",
          name: "cancel",
          className: x["group-btn"],
          textContent: "Cancel"
        }, {
          tag: "button",
          type: "submit",
          name: "addGroup",
          className: x["group-btn"],
          textContent: "Add Group"
        }]
      })]
    })), i.register("groupUsers", (t, e) => g({
      tag: "div",
      className: x.xgroups,
      children: [{
        tag: "div",
        className: x["group-users-list"],
        style: {
          overflowY: "auto",
          maxHeight: "300px"
        },
        children: s.getGroupUsers(t.groupName).map(e => ({
          tag: "div",
          className: x["group-item"],
          children: [{
            tag: "a",
            textContent: "@" + e,
            href: s.getUserLink(e),
            target: "_blank"
          }, {
            tag: "button",
            textContent: "Remove",
            className: x["text-btn"],
            title: "Remove user from group",
            on: {
              click: () => {
                s.removeUserFromGroup(e, t.groupName)
              }
            }
          }]
        }))
      }, {
        className: x["form-footer"],
        children: [{
          tag: "button",
          type: "submit",
          name: "close",
          className: x["group-btn"],
          textContent: "Close",
          on: {
            click: () => {
              i.close()
            }
          }
        }]
      }]
    })), i.register("assignUser", ({
      username: r
    }, e) => g({
      tag: "form",
      className: x.xgroups,
      on: {
        submit: e => {
          e.preventDefault();
          var t = e.target.elements.group.value;
          "cancel" === e.submitter.name ? i.close() : "NEW" === t ? (e = prompt("Enter new group name:")) && (s.addGroup(e), s.addUserToGroup(r, e), i.close()) : t ? (s.addUserToGroup(r, t), i.close()) : _({
            text: "Please select a group."
          })
        }
      },
      children: [new f({
        type: "text",
        label: "Assign User to Group",
        name: "username",
        value: r,
        readOnly: !0,
        style: {
          cursor: "not-allowed"
        }
      }, e).render(), new b({
        name: "group",
        options: [...e.getState().groups.map(e => ({
          label: e.name + " " + e.description,
          value: e.name
        })), {
          label: "Create New",
          value: "NEW"
        }],
        on: {
          change: e => {
            "NEW" === e.target.value && i.open("addGroup", {
              title: "Add New Group"
            })
          }
        }
      }, e).render(), g({
        className: x["form-footer"],
        children: [{
          tag: "button",
          type: "submit",
          name: "cancel",
          className: x["group-btn"],
          textContent: "Cancel"
        }, {
          tag: "button",
          type: "submit",
          name: "assign",
          className: x["group-btn"],
          textContent: "Assign"
        }]
      })]
    })), i.register("removeUser", ({
      username: r
    }, n) => g({
      tag: "form",
      className: x.xgroups,
      on: {
        submit: e => {
          e.preventDefault();
          var t = e.target.elements.group.value;
          "cancel" === e.submitter.name ? i.close() : t ? (s.removeUserFromGroup(r, t), i.close()) : _({
            text: "Please select a group."
          })
        }
      },
      children: [new f({
        type: "text",
        label: "Remove User from Group",
        name: "username",
        value: r,
        readOnly: !0,
        style: {
          cursor: "not-allowed"
        }
      }, n).render(), new b({
        name: "group",
        options: s.getUserGroups(r).map(t => {
          var e = n.getState().groups.find(e => e.name === t);
          return {
            label: e.name + " " + e.description,
            value: e.name
          }
        })
      }, n).render(), g({
        className: x["form-footer"],
        children: [{
          tag: "button",
          type: "submit",
          name: "cancel",
          className: x["group-btn"],
          textContent: "Cancel"
        }, {
          tag: "button",
          type: "submit",
          name: "remove",
          title: "Remove user from group",
          className: x["group-btn"],
          textContent: "Remove"
        }]
      })]
    }));
    class o extends u {
      render() {
        let e = this.props.username;
        var t = s.getUserGroups(e);
        return g({
          className: l(x.xgroups, x["group-tags"]),
          children: [...t.map(t => {
            let e = this.store.getState().groups.find(e => e.name === t);
            return g({
              tag: "span",
              className: x["group-tag"],
              textContent: e.name,
              title: e.description,
              style: {
                background: e.bgColor,
                color: e.fgColor
              },
              on: {
                click: () => i.open("groupUsers", {
                  groupName: e.name,
                  title: "Users in Group: " + e.name,
                  subtitle: g({
                    tag: "button",
                    textContent: "Edit group...",
                    className: x["text-btn"],
                    on: {
                      click: () => i.open("editGroup", {
                        group: e,
                        title: "Edit Group"
                      })
                    }
                  })
                })
              }
            })
          }), {
            className: x["group-tags-btns"],
            children: [{
              tag: "button",
              textContent: "+",
              textContent: "➕",
              title: "Add user to group",
              "aria-label": "Add user to group",
              on: {
                click: () => i.open("assignUser", {
                  username: e,
                  title: "Add to Group"
                })
              }
            }, {
              tag: "button",
              textContent: "-",
              textContent: "➖",
              title: "Remove user from group",
              "aria-label": "- Remove user from group",
              on: {
                click: () => i.open("removeUser", {
                  username: e,
                  title: "Remove User from Group"
                })
              }
            }]
          }]
        })
      }
    }
    let r = e => {
      var t, r;
      e && e.querySelector && (t = e.querySelector(c.usernameSelector) || e.querySelector(c.usernameLinkSelector)) && (t = s.normalizeUsername(t.textContent), e = e.querySelector(c.avatarSelector)) && ((r = e.querySelector("." + x["group-tags"])) && r.remove(), new o({
        username: t
      }, n).mount(e))
    };
    new w({
      title: "Manage XGroups",
      svg: p.groups,
      style: {
        position: "fixed",
        top: "10px",
        right: "10px",
        width: "24px",
        height: "24px",
        zIndex: 1001
      },
      on: {
        click: () => i.open("groupManager", {
          title: "Manage Groups"
        })
      }
    }, n).mount(document.body), s.syncWithGist();
    let a = ((t, r) => {
      let n;
      return (...e) => {
        clearTimeout(n), n = setTimeout(() => t(...e), r)
      }
    })(() => {
      t = !1, console.log("Updating group tags..."), document.querySelectorAll(c.tweetSelector).forEach(r), setTimeout(() => {
        t = !0
      }, 1)
    }, 100);
    return a(), new MutationObserver(e => {
      t && e.forEach(e => {
        Array.from(e.addedNodes).forEach(r)
      })
    }).observe(document.body, {
      childList: !0,
      subtree: !0
    }), n.subscribe(e => {
      (e.groups || e.userGroups) && a()
    }), {
      updateGroupTags: a
    }
  };
  class r {
    constructor() {
      this.store = (e => {
        let t = {
            ...e
          },
          r = new Set;
        return {
          getState: () => t,
          setState: e => {
            t = {
              ...t,
              ...e
            }, r.forEach(e => e(t))
          },
          subscribe: e => (r.add(e), () => r.delete(e))
        }
      })({
        groups: [],
        userGroups: {},
        modalsStack: [],
        loading: !1
      }), this.dataAPI = e(this.store), this.uiManager = t(this.dataAPI, this.store)
    }
  }
  new r
})();