Instagram - Add notes to the user

Add notes (aliases/tags) for users to help identify and search, and support WebDAV sync

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name                Instagram - Add notes to the user
// @name:zh-CN          Instagram - 为用户添加备注(别名/标签)
// @name:zh-TW          Instagram - 為使用者新增備註(別名/標籤)
// @namespace           https://greasyfork.org/zh-CN/users/193133-pana
// @homepage            https://greasyfork.org/zh-CN/users/193133-pana
// @icon                data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBhcmlhLWxhYmVsbGVkYnk9Im5ld0ljb25UaXRsZSIgc3Ryb2tlPSJyZ2JhKDI5LDE2MSwyNDIsMS4wMCkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgZmlsbD0ibm9uZSIgY29sb3I9InJnYmEoMjksMTYxLDI0MiwxLjAwKSI+IDx0aXRsZSBpZD0ibmV3SWNvblRpdGxlIj5OZXc8L3RpdGxlPiA8cGF0aCBkPSJNMTkgMTRWMjJIMi45OTk5N1Y0SDEzIi8+IDxwYXRoIGQ9Ik0xNy40NjA4IDQuMDM5MjFDMTguMjQxOCAzLjI1ODE3IDE5LjUwODIgMy4yNTgxNiAyMC4yODkyIDQuMDM5MjFMMjAuOTYwOCA0LjcxMDc5QzIxLjc0MTggNS40OTE4NCAyMS43NDE4IDYuNzU4MTcgMjAuOTYwOCA3LjUzOTIxTDExLjU4NTggMTYuOTE0MkMxMS4yMTA3IDE3LjI4OTMgMTAuNzAyIDE3LjUgMTAuMTcxNiAxNy41TDcuNSAxNy41TDcuNSAxNC44Mjg0QzcuNSAxNC4yOTggNy43MTA3MSAxMy43ODkzIDguMDg1NzkgMTMuNDE0MkwxNy40NjA4IDQuMDM5MjFaIi8+IDxwYXRoIGQ9Ik0xNi4yNSA1LjI1TDE5Ljc1IDguNzUiLz4gPC9zdmc+
// @version             6.1.8
// @description         Add notes (aliases/tags) for users to help identify and search, and support WebDAV sync
// @description:zh-CN   为用户添加备注(别名/标签)功能,以帮助识别和搜索,并支持 WebDAV 同步功能
// @description:zh-TW   為使用者新增備註(別名/標籤)功能,以幫助識別和搜尋,並支援 WebDAV 同步功能
// @license             GNU General Public License v3.0 or later
// @compatible          chrome
// @compatible          firefox
// @author              pana
// @match               *://*.instagram.com/*
// @require             https://gcore.jsdelivr.net/gh/LightAPIs/greasy-fork-library@da0437e6a856e05158df61225f2b9ea9943ad9ef/Note_Obj.js
// @connect             *
// @noframes
// @grant               GM_info
// @grant               GM_getValue
// @grant               GM_setValue
// @grant               GM_deleteValue
// @grant               GM_listValues
// @grant               GM_openInTab
// @grant               GM_addStyle
// @grant               GM_registerMenuCommand
// @grant               GM_unregisterMenuCommand
// @grant               GM_addValueChangeListener
// @grant               GM_removeValueChangeListener
// ==/UserScript==

(function () {
  'use strict';
  const UPDATED = '2023-04-21';
  const INS_ICON = {
    NOTE_BLACK: 'url(data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBhcmlhLWxhYmVsbGVkYnk9Im5ld0ljb25UaXRsZSIgc3Ryb2tlPSJyZ2IoMzgsIDM4LCAzOCkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgZmlsbD0ibm9uZSIgY29sb3I9InJnYigzOCwgMzgsIDM4KSI+IDx0aXRsZSBpZD0ibmV3SWNvblRpdGxlIj5OZXc8L3RpdGxlPiA8cGF0aCBkPSJNMTkgMTRWMjJIMi45OTk5N1Y0SDEzIi8+IDxwYXRoIGQ9Ik0xNy40NjA4IDQuMDM5MjFDMTguMjQxOCAzLjI1ODE3IDE5LjUwODIgMy4yNTgxNiAyMC4yODkyIDQuMDM5MjFMMjAuOTYwOCA0LjcxMDc5QzIxLjc0MTggNS40OTE4NCAyMS43NDE4IDYuNzU4MTcgMjAuOTYwOCA3LjUzOTIxTDExLjU4NTggMTYuOTE0MkMxMS4yMTA3IDE3LjI4OTMgMTAuNzAyIDE3LjUgMTAuMTcxNiAxNy41TDcuNSAxNy41TDcuNSAxNC44Mjg0QzcuNSAxNC4yOTggNy43MTA3MSAxMy43ODkzIDguMDg1NzkgMTMuNDE0MkwxNy40NjA4IDQuMDM5MjFaIi8+IDxwYXRoIGQ9Ik0xNi4yNSA1LjI1TDE5Ljc1IDguNzUiLz4gPC9zdmc+)'
  };
  const selector = {
    homepage: {
      article: '[role="main"] article',
      id: '._aaqt a',
      icon: 'span._aamz',
      commentId: '._ab8x .xt0psk2 a.notranslate',
      commentAt: '._ab8x ._aacl a.notranslate'
    },
    homepageStories: {
      id: '._aad6',
      idShell: 'li [role="menuitem"]'
    },
    homepageRecommend: {
      id: '._aak3 ._ab8x .xt0psk2 a'
    },
    userPage: {
      frame: '._aa_y',
      id: 'h2',
      bar: '.x8j4wrb',
      box: 'ul',
      common: 'span._aaai',
      suggest: '._acj1 a.notranslate',
      infoAt: '.notranslate',
      userName: '._aa_c > span'
    },
    watchList: {
      initialItem: '[role="dialog"] [aria-labelledby]',
      laterItem: '._aaei',
      id: '._ab8w a.notranslate'
    },
    stories: {
      id: 'a.notranslate',
      idShell: '._ac0o',
      cardId: '._afgd ._aacw'
    },
    dialog: {
      frame: '[role="dialog"] article',
      commentId: '._a9zc ._ab8w a, ._aacx._aacu a',
      commentAt: '._a9zs .notranslate'
    },
    request: {
      follow: '._aajc ._ab8x .xt0psk2 a'
    },
    suggest: {
      user: '._aa0- ._ab8x .xt0psk2 a'
    }
  };
  const nameSet = {
    backgroundBox: 'note-obj-ins-background-box',
    userpageTag: 'note-obj-ins-userpage-tag',
    fontBold: 'note-obj-ins-font-bold',
    addBtn: 'note-obj-ins-add-btn',
    homepageBtn: 'note-obj-ins-homepage-btn',
    userpageBtn: 'note-obj-ins-userpage-btn'
  };
  const INS_STYLE = `
    .${nameSet.backgroundBox} {
      display: inline-block;
      align-items: center;
      white-space: nowrap;
      border-radius: 50px;
      padding: 0px 10px;
      background-color: #336699;
      color: #fff;
    }
    .${nameSet.addBtn} {
      background-image: ${INS_ICON.NOTE_BLACK};
      background-size: 24px;
      background-repeat: no-repeat;
      background-position: center;
      margin-left: 5px;
      cursor: pointer;
      width: 24px;
      height: 24px;
    }
    .${nameSet.homepageBtn} {
      margin: 6px !important;
    }
    .${nameSet.homepageBtn}:hover {
      opacity: 0.5;
    }
    .${nameSet.userpageBtn} {
      margin-top: 2px;
    }
    .${nameSet.userpageTag} {
      display: block;
      font-size: 20px;
      margin-bottom: 20px;
      white-space: nowrap;
    }
    .${nameSet.fontBold} {
      font-weight: bold;
    }
    .note-obj-settings-frame-card label {
      color: #2f2f2f;
    }
    .note-obj-interface-dark .note-obj-settings-frame-card label {
      color: #fff;
    }
    .note-obj-group-frame-tbody input,
    .note-obj-webdav-frame-form-item input {
      color: #000;
    }`;
  const noteObj = new Note_Obj({
    id: 'myInstagramNote',
    script: {
      author: {
        name: 'pana',
        homepage: 'https://greasyfork.org/zh-CN/users/193133-pana'
      },
      url: 'https://greasyfork.org/scripts/387871',
      updated: UPDATED
    },
    style: INS_STYLE,
    primaryColor: '#336699',
    settings: {
      replaceHomepageID: {
        type: 'checkbox',
        lang: {
          en: 'Allow replacing user IDs on the home page',
          zhHans: '允许替换首页上的用户 ID',
          zhHant: '允許替換首頁上的使用者 ID'
        },
        default: true,
        event: instagramHomepageEvent
      }
    },
    changeEvent: instagramChangeEvent
  });
  function homepageNote(ele, changeId) {
    const user = noteObj.fn.queryAnchor(ele, selector.homepage.id);
    if (user) {
      const replaceHomepageID = noteObj.getOtherConfig().replaceHomepageID === true;
      const eleId = noteObj.fn.getIdFromUrl(user.href);
      if (!changeId || changeId === eleId) {
        noteObj.handler(eleId, user, undefined, {
          add: replaceHomepageID ? undefined : 'sapn',
          className: replaceHomepageID ? undefined : [nameSet.backgroundBox]
        });
      }
    }
  }
  function homepageCommentNote(ele, changeId) {
    for (const comment of noteObj.fn.queryAllAnchor(ele, selector.homepage.commentId, 'info')) {
      const commentId = noteObj.fn.getIdFromUrl(comment.href);
      if (!changeId || changeId === commentId) {
        noteObj.handler(commentId, comment);
      }
    }
  }
  function homepageCommentAtNote(ele, changeId) {
    const commentAtId = noteObj.fn.getIdFromUrl(ele.href);
    if (!changeId || changeId === commentAtId) {
      noteObj.handler(commentAtId, ele, undefined, {
        prefix: '@',
        title: true
      });
    }
  }
  function dialogCommentNote(ele, chagneId) {
    const picCommentId = noteObj.fn.getIdFromUrl(ele.href);
    if (!chagneId || chagneId === picCommentId) {
      noteObj.handler(picCommentId, ele);
    }
  }
  function dialogCommentAtNote(ele, changeId) {
    if (!ele.classList.contains(selector.homepage.commentId.replace(/^\.|\s+.*$/g, ''))) {
      const picCommentAtId = noteObj.fn.getIdFromUrl(ele.href);
      if (!changeId || changeId === picCommentAtId) {
        noteObj.handler(picCommentAtId, ele, undefined, {
          prefix: '@',
          title: true
        });
      }
    }
  }
  function homepageStoriesNote(ele, changeId) {
    const homepageStoriesId = noteObj.fn.getText(ele, selector.homepageStories.id);
    if (!changeId || changeId === homepageStoriesId) {
      ele.title = noteObj.getUserTag(homepageStoriesId);
    }
  }
  function anchorElementNote(ele, changeId) {
    const itemId = noteObj.fn.getIdFromUrl(ele.href);
    if (!changeId || changeId === itemId) {
      noteObj.handler(itemId, ele);
    }
  }
  function userPageNote(ele, changeId) {
    const userPageId = noteObj.fn.getText(ele, selector.userPage.id);
    const userPageBox = noteObj.fn.query(ele, selector.userPage.box);
    if (userPageId && userPageBox) {
      if (changeId) {
        if (changeId === userPageId) {
          noteObj.handler(userPageId, ele, undefined, {
            add: 'div',
            after: userPageBox,
            maskSecondaryColor: true,
            offsetWidth: -20,
            className: [nameSet.userpageTag, nameSet.fontBold]
          });
        }
      } else {
        const userNameText = noteObj.fn.getText(ele, selector.userPage.userName, 'warn');
        noteObj.handler(userPageId, ele, undefined, {
          add: 'div',
          after: userPageBox,
          maskSecondaryColor: true,
          offsetHeight: -20,
          className: [nameSet.userpageTag, nameSet.fontBold]
        }, userNameText);
      }
    }
  }
  function userPageCommonNote(ele, changeId) {
    for (const commonUser of noteObj.fn.queryAll(ele, selector.userPage.common, 'info')) {
      const commonUserId = commonUser.textContent?.trim();
      if (commonUserId) {
        if (!changeId || changeId === commonUserId) noteObj.handler(commonUserId, commonUser, undefined, {
          title: true,
          notModify: true
        });
      }
    }
  }
  function userPageInfoAtNote(ele, changeId) {
    for (const infoAtUser of noteObj.fn.queryAllAnchor(ele, selector.userPage.infoAt, 'info')) {
      const infoAtUserId = noteObj.fn.getIdFromUrl(infoAtUser.href);
      if (!changeId || changeId === infoAtUserId) {
        noteObj.handler(infoAtUserId, infoAtUser, undefined, {
          prefix: '@',
          title: true
        });
      }
    }
  }
  function storiesNote(ele, changeId) {
    itemNote(ele, selector.stories.id, changeId);
    noteObj.fn.queryAll(selector.stories.cardId).forEach(item => {
      const itemId = item.textContent?.trim() || '';
      if (!changeId || changeId === itemId) {
        noteObj.handler(itemId, item, undefined, {
          notModify: true,
          title: true
        });
      }
    });
  }
  function watchListItemNote(ele, changeId) {
    itemNote(ele, selector.watchList.id, changeId);
  }
  function itemNote(ele, idSelector, changeId) {
    const item = noteObj.fn.queryAnchor(ele, idSelector);
    if (item) {
      const itemId = noteObj.fn.getIdFromUrl(item.href);
      if (!changeId || changeId === itemId) {
        noteObj.handler(itemId, item);
      }
    }
  }
  function instagramChangeEvent(changeId) {
    for (const article of noteObj.fn.queryAll(selector.homepage.article, 'none')) {
      homepageNote(article, changeId);
      homepageCommentNote(article, changeId);
      for (const commentAt of noteObj.fn.queryAllAnchor(selector.homepage.commentAt, 'none')) {
        homepageCommentAtNote(commentAt, changeId);
      }
      for (const picCommentUser of noteObj.fn.queryAllAnchor(selector.dialog.commentId, 'none')) {
        dialogCommentNote(picCommentUser, changeId);
      }
      for (const picCommentAt of noteObj.fn.queryAllAnchor(selector.dialog.commentAt, 'none')) {
        dialogCommentAtNote(picCommentAt, changeId);
      }
    }
    for (const homepageStories of noteObj.fn.queryAll(selector.homepageStories.idShell, 'none')) {
      homepageStoriesNote(homepageStories, changeId);
    }
    for (const homepageRecommend of noteObj.fn.queryAllAnchor(selector.homepageRecommend.id, 'none')) {
      anchorElementNote(homepageRecommend, changeId);
    }
    for (const userPage of noteObj.fn.queryAll(selector.userPage.frame, 'none')) {
      userPageNote(userPage, changeId);
      userPageCommonNote(userPage, changeId);
      userPageInfoAtNote(userPage, changeId);
    }
    for (const storiesShell of noteObj.fn.queryAll(selector.stories.idShell, 'none')) {
      storiesNote(storiesShell, changeId);
    }
    for (const initial of noteObj.fn.queryAll(selector.watchList.initialItem, 'none')) {
      watchListItemNote(initial, changeId);
    }
    for (const later of noteObj.fn.queryAll(selector.watchList.laterItem, 'none')) {
      watchListItemNote(later, changeId);
    }
    for (const dialog of noteObj.fn.queryAll(selector.dialog.frame, 'none')) {
      homepageNote(dialog, changeId);
      homepageCommentNote(dialog, changeId);
      for (const commentUser of noteObj.fn.queryAllAnchor(selector.dialog.commentId, 'none')) {
        dialogCommentNote(commentUser, changeId);
      }
      for (const commentAt of noteObj.fn.queryAllAnchor(selector.dialog.commentAt, 'none')) {
        dialogCommentAtNote(commentAt, changeId);
      }
    }
    for (const follow of noteObj.fn.queryAllAnchor(selector.request.follow, 'none')) {
      anchorElementNote(follow, changeId);
    }
    for (const suggestUser of noteObj.fn.queryAllAnchor(selector.suggest.user, 'none')) {
      anchorElementNote(suggestUser, changeId);
    }
    for (const suggest of noteObj.fn.queryAllAnchor(selector.userPage.suggest, 'none')) {
      anchorElementNote(suggest, changeId);
    }
  }
  function instagramHomepageEvent(newValue, oldValue) {
    for (const article of noteObj.fn.queryAll(selector.homepage.article)) {
      const articleUser = noteObj.fn.queryAnchor(article, selector.homepage.id);
      if (articleUser) {
        const articleUserId = noteObj.fn.getIdFromUrl(articleUser.href);
        noteObj.handler(articleUserId, articleUser, undefined, {
          add: oldValue ? undefined : 'span',
          className: oldValue ? undefined : [nameSet.backgroundBox],
          title: oldValue,
          restore: true
        });
        noteObj.handler(articleUserId, articleUser, undefined, {
          add: newValue ? undefined : 'span',
          className: newValue ? undefined : [nameSet.backgroundBox],
          title: newValue
        });
      }
    }
  }
  function initInstagram() {
    const arriveOption = {
      fireOnAttributesModification: true,
      existing: true
    };
    noteObj.arrive(document.body, selector.homepage.article, arriveOption, article => {
      const homepageIcon = noteObj.fn.query(article, selector.homepage.icon);
      const articleUserId = noteObj.fn.getUrlId(article, selector.homepage.id);
      if (homepageIcon && articleUserId) {
        homepageIcon.insertAdjacentElement('beforebegin', noteObj.createNoteBtn(articleUserId, undefined, [nameSet.addBtn, nameSet.homepageBtn], 'span'));
      }
      homepageNote(article);
      homepageCommentNote(article);
      noteObj.arrive(article, selector.homepage.commentAt, arriveOption, commentAt => {
        homepageCommentAtNote(commentAt);
      });
      noteObj.arrive(article, selector.dialog.commentId, arriveOption, picCommentUser => {
        dialogCommentNote(picCommentUser);
      });
      noteObj.arrive(article, selector.dialog.commentAt, arriveOption, picCommentAt => {
        dialogCommentAtNote(picCommentAt);
      });
    });
    noteObj.arrive(document.body, selector.homepageStories.idShell, arriveOption, homepageStories => {
      homepageStoriesNote(homepageStories);
    });
    noteObj.arrive(document.body, selector.homepageRecommend.id, arriveOption, homepageRecommend => {
      anchorElementNote(homepageRecommend);
    });
    noteObj.arrive(document.body, selector.userPage.frame, arriveOption, userPage => {
      const userPageBar = noteObj.fn.query(userPage, selector.userPage.bar);
      const userPageId = noteObj.fn.getText(userPage, selector.userPage.id);
      if (userPageBar && userPageId) {
        const userNameText = noteObj.fn.getText(userPage, selector.userPage.userName, 'info');
        userPageBar.after(noteObj.createNoteBtn(userPageId, userNameText, [nameSet.addBtn, nameSet.userpageBtn]));
      }
      userPageNote(userPage);
      userPageCommonNote(userPage);
      userPageInfoAtNote(userPage);
    });
    noteObj.arrive(document.body, selector.stories.idShell, arriveOption, storiesShell => {
      storiesNote(storiesShell);
      const stories = noteObj.fn.queryAnchor(storiesShell, selector.stories.id);
      if (stories) {
        const userIdChange = new MutationObserver(() => {
          const newUserId = noteObj.fn.getIdFromUrl(stories.href);
          if (noteObj.judgeUsers(newUserId)) {
            noteObj.handler(newUserId, stories);
          } else {
            noteObj.handler(newUserId, stories, undefined, {
              restore: true
            });
          }
        });
        userIdChange.observe(stories, {
          attributeFilter: ['href']
        });
      }
    });
    noteObj.arrive(document.body, selector.watchList.initialItem, arriveOption, initial => {
      watchListItemNote(initial);
    });
    noteObj.arrive(document.body, selector.watchList.laterItem, arriveOption, later => {
      watchListItemNote(later);
    });
    noteObj.arrive(document.body, selector.dialog.frame, arriveOption, dialog => {
      const homepageIcon = noteObj.fn.query(dialog, selector.homepage.icon);
      const dialogUserId = noteObj.fn.getUrlId(dialog, selector.homepage.id);
      if (homepageIcon && dialogUserId) {
        homepageIcon.insertAdjacentElement('beforebegin', noteObj.createNoteBtn(dialogUserId, undefined, [nameSet.addBtn, nameSet.homepageBtn], 'span'));
      }
      homepageNote(dialog);
      homepageCommentNote(dialog);
      noteObj.arrive(dialog, selector.dialog.commentId, arriveOption, commentUser => {
        dialogCommentNote(commentUser);
      });
      noteObj.arrive(dialog, selector.dialog.commentAt, arriveOption, commentAt => {
        dialogCommentAtNote(commentAt);
      });
    });
    noteObj.arrive(document.body, selector.request.follow, arriveOption, follow => {
      anchorElementNote(follow);
    });
    noteObj.arrive(document.body, selector.suggest.user, arriveOption, suggestUser => {
      anchorElementNote(suggestUser);
    });
    noteObj.arrive(document.body, selector.userPage.suggest, arriveOption, suggest => {
      anchorElementNote(suggest);
    });
  }
  initInstagram();
})();