仕事気分でツイッターしようぜ！！！！

# 仕事気分でツイッターがしたい
Twitterは楽しい．ただ一つ難点があるとするのなら **時間の浪費を感じる** ．
TwitterはTwitterであるが故，時間の浪費をしている子尾を感じてしまう．
せっかく楽しいTwitterを負い目なく楽しみたい．

まずその原因はTwitterの見た目がTwitterっぽすぎるのだ．
つまりあの画面を見て「仕事してるな」と思うことはできない．
なので自分をだますために仕事で見る画面のデザインに変えてしまおう！！
ということでChrome拡張を作りました

最終的にはこんな感じ
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">罪悪感なくツイッターするために仕事の気分でできるツイッターを作りました <a href="https://t.co/DiXHzxdbJz">pic.twitter.com/DiXHzxdbJz</a></p>&mdash; 電電猫猫@CoeFont (@nya3_neko2) <a href="https://twitter.com/nya3_neko2/status/1620711050839756801?ref_src=twsrc%5Etfw">February 1, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

お試しはChrome拡張のWebstoreに
使うときにはTwitterの方の表示デザインをブラックにしてください

https://chrome.google.com/webstore/detail/fakelooktwitterchromeexte/dncgbpnoglgdnigmalleleelehdblloc

GitHubはこっちね

https://github.com/nyanko3141592/fakeLookTwitter

## Chrome拡張を作ろう

今回のmanifest.jsonを作ります．ざっくりchromeに読み込むためにChrome拡張の権限やらなにやらの宣言ファイルです．

<details><summary>manifest.json</summary>

```
{
  "name": "FakeLookTwitterChromeExtension",
  "version": "1.1.1",
  "description": "Not Twitte Like Look Twitter Design Custom",
  "manifest_version": 3,
  "icons": {
    "16": "asset/logo16.png",
    "48": "asset/logo48.png",
    "128": "asset/logo128.png"
  },
  "content_scripts": [
    {
      "matches": ["https://twitter.com/*"],
      "js": ["js/jquery-3.6.3.min.js", "js/content.ts"],
      "exclude_matches": ["https://twitter.com/messages"],
      "css": ["css/content.css", "css/injectedFile.css"],
      "run_at": "document_end"
    }
  ],
  "web_accessible_resources": [
    {
      "resources": ["html/*.html"],
      "matches": ["https://twitter.com/*"]
    }
  ]
}
```

今はmanifest version3が推奨なので3でいきます．詳細はGoogle公式ドキュメントを読もう．
Chrome拡張の動作するURLは``"matches": ["https://twitter.com/*"]``で決めます．
DMのデザインはめんどくさかったので， DMは除外します．`` "exclude_matches": ["https://twitter.com/messages"],``
今回はローカルのHTMLファイルをJSで読んでサイトに無理やりぶちこみます
</details>

メインとなるcontent.js

<details><summary>content.js</summary>

最初っからTSにしとけばよかったな
```js
var searchForm;
var saveChannel;
var isChannelShow = true;
var workspaceTitle = "Slacker";
const channelTitleMaxLength = 19;
const defaultChannel = [
  ["home", "/home"],
  ["notification", "/notifications"],
  ["explore", "/explore"],
  ["bookmark", "/i/bookmarks"],
  ["profile", "/"],
];

if (localStorage.getItem("saveChannel") != null) {
  saveChannel = JSON.parse(localStorage.getItem("saveChannel"));
}
if (localStorage.getItem("workspaceTitle") != null) {
  workspaceTitle = localStorage.getItem("workspaceTitle");
}

function isDefaultChannelURL(checkURL) {
  defaultChannel.forEach((element) => {
    if (checkURL == element[1]) {
      return true;
    }
  });
  return false;
}

function addHtmlToBody(htmlPath) {
  // jquery load text file
  path = chrome.runtime.getURL(htmlPath);
  // dataが読み込まれるまで待つ
  var htmlText = $.ajax({
    url: path,
    async: false,
  }).responseText;
  // get the body element
  var body = document.getElementsByTagName("body")[0];
  var div = document.createElement("div");
  // change background color
  div.innerHTML = htmlText;
  body.appendChild(div);
  return div;
}

function twitterSearch(e) {
  if (e.keyCode === 13) {
    window.location.href = "https://twitter.com/search?q=" + searchForm.value;
  }
  return false;
}

function makeChannelTitle(url) {
  var a = url.replace("https://twitter.com/", "");
  if (a == "") {
    a = "home";
  } else if (a == "home") {
    a = "home";
  } else if (a == "notifications") {
    a = "notification";
  } else if (a == "explore") {
    a = "explore";
  } else if (a == "i/bookmarks") {
    a = "bookmark";
  } else if (url.includes("search")) {
    a = url.replace("https://twitter.com/search?q=", "");
  } else if (url.includes("hashtag")) {
    a = url.replace("https://twitter.com/hashtag/", "");
  } else if (url.includes("lists")) {
    a = "list";
  } else if (url.includes("moments")) {
    a = "moment";
  } else if (!a.includes("/")) {
    a = "times_" + url.replace("https://twitter.com/", "");
  }
  if (a.includes("?")) {
    a = a.split("?")[0];
  } else if (a.includes("&")) {
    a = a.split("&")[0];
  } else if (a.includes("/status")) {
    a = a.split("/status")[0];
  }
  return decodeUrlEncodedString(a);
}

// add channel
function addChannelUI(elementArray) {
  console.log(elementArray);
  var channel = document.createElement("li");
  var channelLink = document.createElement("a");
  var deleteButton = document.createElement("button");
  channelLink.href = elementArray[1];
  // set channel name
  if (elementArray[0].length > channelTitleMaxLength) {
    channelLink.innerText =
      "# " + elementArray[0].slice(0, channelTitleMaxLength) + "...";
  } else {
    channelLink.innerText = "# " + elementArray[0];
  }
  // set event lister
  document.getElementById("channelList").appendChild(channel);
  if (elementArray[0] == "profile") {
    console.log("profile")
    channelLink.id = "profile";
    channel.addEventListener("click", function () {
      document
      .querySelectorAll('[data-testid="AppTabBar_Profile_Link"]')[0]
      .click();
    });
  } else {
    document.getElementById("channelList").appendChild(channel);
    // li 全体にリンクを追加
    channel.addEventListener("click", function () {
      window.location.href = elementArray[1];
    });
  }
  channel.appendChild(channelLink);
  // add delete button
  deleteButton.innerText = "×";
  deleteButton.className = "deleteButton";
  deleteButton.addEventListener("click", function () {
    // delete channel
    for (let i = 0; i < saveChannel.length; i++) {
      if (saveChannel[i][0] == elementArray[0]) {
        // confirm
        if (!confirm("delete " + elementArray[0] + "channel?")) {
          return;
        }
        saveChannel.splice(i, 1);
        localStorage.setItem("saveChannel", JSON.stringify(saveChannel));
        break;
      }
    }
    // delete channel UI
    channel.remove();
  });
  channel.appendChild(deleteButton);
  // delete button display check
  defaultChannel.forEach((element) => {
    if (elementArray[1] == element[1]) {
      deleteButton.style.display = "none";
      return;
    }
  });
}

function backButton() {
  window.history.back();
}

function addchannel() {
  const nowURL = window.location.href;
  // 既に追加されているかチェック:デフォルトチャンネル
  if (isDefaultChannelURL(nowURL)) {
    alert("already added");
    return;
  }
  // 既に追加されているかチェック：追加分
  for (let i = 0; i < saveChannel.length; i++) {
    if (
      saveChannel[i][1] == nowURL ||
      saveChannel[i][0] == makeChannelTitle(nowURL)
    ) {
      alert("already added");
      return;
    }
  }
  saveChannel.push([makeChannelTitle(nowURL), nowURL]);
  localStorage.setItem("saveChannel", JSON.stringify(saveChannel));
  addChannelUI(saveChannel[saveChannel.length - 1]);
  alert("channel added");
}

function setChannelTitle() {
  // チャンネルタイトルを表示
  document.getElementById("channelTitle").innerText =
    "# " + makeChannelTitle(window.location.href);
}

window.onload = function () {
  addHtmlToBody("html/sidebar.html").style.height =
    window.innerHeight - 44 + "px";
  addHtmlToBody("html/searchBar.html");
  addHtmlToBody("html/channelHeader.html");
  document.getElementById("backButton").addEventListener("click", backButton);
  // 検索バーのイベントリスナーを追加
  searchForm = document.getElementById("searchInputForm");
  searchForm.addEventListener("keypress", twitterSearch);
  // channel 追加
  if (saveChannel == null || saveChannel == undefined) {
    saveChannel = defaultChannel;
  }
  saveChannel.forEach((element) => {
    addChannelUI(element);
  });
  // addchannel event listener
  var addchannelButton = document.getElementById("addChannel");
  addchannelButton.addEventListener("click", addchannel);
  // チャンネルタイトルを表示
  setChannelTitle();
  // workspace title
  workspaceTitleElement = document.getElementById("workspaceTitle");
  workspaceTitleElement.value = workspaceTitle;
  workspaceTitleElement.placeholder = workspaceTitle;
  workspaceTitleElement.addEventListener("keypress", function () {
    workspaceTitle = workspaceTitleElement.value;
    localStorage.setItem("workspaceTitle", workspaceTitle);
    workspaceTitleElement.placeholder = workspaceTitle;
  });
  // set toggle
  document
    .getElementById("channelToggle")
    .addEventListener("click", switchChannels);
  // get ol id funcList
  var funcList = document.getElementById("funcList");
  // set event listener each li
  for (let i = 0; i < funcList.children.length; i++) {
    funcList.children[i].addEventListener("click", function () {
      window.location.href = funcList.children[i].children[0].href;
    });
  }
};

window.onhashchange = function () {
  document.getElementById("channelTitle").innerText = makeChannelTitle(
    window.location.href
  );
};

// decode URL encoded string
function decodeUrlEncodedString(str) {
  return decodeURIComponent(str.replace(/\+/g, " "));
}

window.addEventListener(
  "click",
  function () {
    setChannelTitle();
  },
  false
);

window.addEventListener("popstate", (e) => {
  setChannelTitle();
});

function switchChannels() {
  // display none
  isChannelShow = !isChannelShow;
  if (isChannelShow) {
    document.getElementById("channelList").style.display = "block";
    document.getElementById("channelToggle").innerText = "▼ Channels";
  } else {
    document.getElementById("channelList").style.display = "none";
    document.getElementById("channelToggle").innerText = "▶ Channels";
  }
}

```

大事なのはここくらいなもん

```js
function addHtmlToBody(htmlPath) {
  path = chrome.runtime.getURL(htmlPath);
  var htmlText = $.ajax({
    url: path,
    async: false,
  }).responseText;
  var body = document.getElementsByTagName("body")[0];
  var div = document.createElement("div");
  div.innerHTML = htmlText;
  body.appendChild(div);
  return div;
}
```
``path = chrome.runtime.getURL(htmlPath);`` これでchrome拡張においたHTMLを読み込める．
でもこれだけだとChrome拡張が権限なくてHTML読んでくれないっぽいので，manifest.jsonでアクセスできるようにします

```
  "web_accessible_resources": [
    {
      "resources": ["html/*.html"],
      "matches": ["https://twitter.com/*"]
    }
```

</details>

あとはHTMLとcss(sass)は諸々とasset

## Chrome拡張を読み込もう

拡張機能を管理のところからパッケージ化されてないの読み込みます
manifest.jsonミスるとここで文句言われて読み込まれません

## 完成

![サムネ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263501/ed95bb05-53e1-3beb-b0e0-a48bfe1b8009.jpeg)

### 機能紹介

- 見た目が某チャットアプリ風
- 検索バーから検索が可能
- サイドバーから下書き・予約済み・通知・メンションなど細かくアクセスできる
- 任意のTwitterのページをaddchannelからサイドバーに追加できる
    - 個人のページだとtimes_{user id}になる
- Workspace名を任意に変更できる

つくって意外と便利だったのがtimesのchannel追加機能．よく見るエゴサとかの検索とか保存しとくと便利

## おわり！

だいたい満足した．js書くの数年ぶり過ぎてなんも分からんかった．あとmanifest_versionが3でちょくちょく変わってて知らないのも多かった
ひとまずのお試ししてみて．バグってたらごめん

https://chrome.google.com/webstore/detail/fakelooktwitterchromeexte/dncgbpnoglgdnigmalleleelehdblloc