Chienomi

Chienomiマイページ、爆誕

開発::website

Chienomiマイページは、Chienomiの閲読管理用のページである。

コンテンツのメインは目次であり、その意味でindexページと変わらないが、personalizationの要素を取り込んでいる。 現状では、閲読したページのマークと、カテゴリによるフィルタの2つだ。 具体的なプランがあるわけではないが、将来的にはより便利な機能が追加される可能性はある。リクエスト次第でもあるだろう。

このページはSPAのようなものだが、サーバーに送信するものは何もない。 ページを取得したら、あとはページ内のJavaScriptで完結している。

既読マーク、およびフィルタ設定はLocalStorageに保存されている。 このために、ページ閲読時に既読情報をLocalStorageに書くようになった。

トラッキングしない、というのはChienomiとしてユーザーとの約束事なので、将来的にもこの情報が同期可能になる予定はない。 ただ、サーバーとの通信を必要とする高度な処理がなければ使われないようであれば、マイページに限ってユーザー情報をサーバーに持つような構造に変更する可能性はある。 これは、その場合でも閲読時に閲読情報をサーバーに送るわけではなく、マイページを開いたときに閲読情報を同期するという意味だ。

目的

Chienomiマイページは特に要望があったから作ったわけではなく、私が考えているいくつかの拡張の中で下地になりそうだという理由で作ったものだ。

Chienomiには「更新通知」という問題がある。

Chienomiを愛読してくれている読者というのがいることは把握しており、いくつかの更新通知手段を用意している。 推奨されているのはTelegramチャンネルだが、登録者はわずか6人に過ぎない。

そのほか、Atomフィードもあるが、今どきAtomを購読している人は多くないと思われる。

Wordpress.comのメール配信がされていたときは300人近い登録者がいたはずだが、その人たちはどこへ。

そしてこの度、Sendgridを介してメール配信が可能になった。 なので、更新通知メールも出そうと思えば出せるようになったのだけど、メールアドレスの本人確認とspam防止、購読の管理などを提供すると非常に面倒なので、結局手を付けていない。

予定としては次はウェブ通知を目標にしている(これもこれでとてもめんどくさい)のだけど、ウェブ通知を入れる場合もセットであると色々応用が効くかなと考えている。

コード

既読チェック部分はbase.jsに追加されたこのコードで実現している。

(function() {
  var ChienomiStorage = window.localStorage
  var userDB = {}
  var url = "/" + location.href.replace(/.*?chienomi.org\//, "").replace(/[?#].*/, "")
  if (ChienomiStorage.getItem("read")) {
    userDB.read = JSON.parse(ChienomiStorage.getItem("read"))
  } else {
    userDB.read = {}
  }
  var now = new Date()
  userDB.read[url] = (("0000" + now.getFullYear()).slice(-4) + "-" + ("00" + (now.getMonth() + 1)).slice(-2) + "-" + ("00" + now.getDate()).slice(-2))
  ChienomiStorage.setItem("read", JSON.stringify(userDB.read))
})()

MyPage側はDLSite Voice Utilsに近い感じだ。 HTMLファイルは本当に箱だけ。

<html lang="ja">
  <head>
    <title>Chienomi MyPage</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
    <meta charset="utf-8" />
    <link rel="stylesheet" type="text/css" href="/mypage/skin.css"></style>
    <script src="/mypage/main.js" defer></script>
  </head>
  <body>
    <nav id="Filter">
      <form id="FilterForm">
        <fieldset id="FormCategories">
          <legend>Categories</legend>
        </fieldset>
      </form>
    </nav>
    <table id="MainTable">
      <thead id="MainTableHeader"><th>Title</th><th>Category</th><th class="date">Pub/Read</th><th class="semi_wide date">Last Update</th><th class="wide">Updated</th></thead>
      <tbody id="MainTableBody"></tbody>
    </table>
    <footer>
      <ul>
        <li><a href="//chienomi.org/about/mypage.html">マイページについて</a></li>
      </ul>
    </footer>
  </body>
</html>

JavaScriptのほうは長いけれど、やっていることはとても単純。 一覧の更新はcreateTable()を呼べばOK。 若干の力技が光る。

var articles
var ChienomiStorage = window.localStorage
var userDB = {}
var chienomiFilter = {}

async function chienomiStorageLoad() {
  if (ChienomiStorage.getItem("read")) {
    userDB.read = JSON.parse(ChienomiStorage.getItem("read"))
  } else {
    userDB.read = {}
    ChienomiStorage.setItem("read", JSON.stringify(userDB.read))
  }

  if (ChienomiStorage.getItem("filter")) {
    userDB.filter = JSON.parse(ChienomiStorage.getItem("filter"))
  } else {
    userDB.filter = {categories: []}
    ChienomiStorage.setItem("filter", JSON.stringify(userDB.filter))
  }
}

async function chienomiLoad() {
  articles = await (await fetch("/articles.json")).json()

  const filterForm = document.getElementById("FilterForm")

  const categories = new Set()
  for (const i of articles) {
    i.category = i.category_spec.replace(/::.*/, "")
    categories.add(i.category)
  }
  for (const i of categories) {
    const opt = document.createElement("input")
    opt.type = "checkbox"
    if (userDB.filter.categories.includes(i)) {
      opt.checked = true
    }
    opt.name = i
    const label = document.createElement("label")
    label.appendChild(opt)
    label.appendChild(document.createTextNode(i))
    filterForm.FormCategories.appendChild(label)
  }
}

async function createTable() {
  const filter = userDB.filter

  const tbody = document.getElementById("MainTableBody")
  const new_body = document.createElement("tbody")
  new_body.id = "MainTableBody"
  for (const i of articles) {
    // Check filter
    if (filter.categories.length > 0) {
      if (!filter.categories.includes(i.category_spec.replace(/::.*/, ""))) { continue }
    }

    const tr = document.createElement("tr")

    const title = document.createElement("td")
    const title_link = document.createElement("a")
    if (userDB.read[i.url] && (!i.last_update || i.last_update.date < userDB.read[i.url])) {
      title_link.className = "read"
    }
    title_link.href = "//chienomi.org" + i.url
    title.appendChild(title_link)
    const title_text = document.createTextNode(i.title)
    title_link.appendChild(title_text)
    tr.appendChild(title)

    const category = document.createElement("td")
    const category_text = document.createTextNode(i.category_spec)
    category.appendChild(category_text)
    tr.appendChild(category)

    const date = document.createElement("td")
    let date_text
    if (userDB.read[i.url] && (!i.last_update || i.last_update.date < userDB.read[i.url])) {
      date_text = document.createTextNode(userDB.read[i.url])
      date.className = "read pre"
    } else {
      date_text = document.createTextNode(i.date.replace(/ .*$/, ""))
      date.className = "pre"
    }
    date.appendChild(date_text)
    tr.appendChild(date)

    const lu_date = document.createElement("td")
    lu_date.className = "semi_wide pre"
    if (i.last_update) {
      const lu_date_text = document.createTextNode(i.last_update?.date)
      lu_date.appendChild(lu_date_text)
    }
    tr.appendChild(lu_date)

    const lu_desc = document.createElement("td")
    lu_desc.className = "wide"
    if (i.last_update) {
      const lu_desc_text = document.createTextNode(i.last_update?.desc)
      lu_desc.appendChild(lu_desc_text)
    }
    tr.appendChild(lu_desc)

    new_body.appendChild(tr)
  }
  tbody.replaceWith(new_body)
}

async function chienomiMain() {
  await chienomiStorageLoad()
  await chienomiLoad()
  await createTable()
}

document.getElementById("FilterForm").addEventListener("change", e => {
  const category_inputs = document.getElementById("FilterForm").FormCategories.getElementsByTagName("input")
  const filter = userDB.filter
  const categories = new Set()
  for (const i of category_inputs) {
    if (i.checked) {
      categories.add(i.name)
    }
  }
  filter.categories = Array.from(categories)
  ChienomiStorage.setItem("filter", JSON.stringify(userDB.filter))
  createTable()
})

chienomiMain()

DLSite Voice Utilsと違って、ちゃんとdocument.createTextNodeしてる。

マイページのほうは動かなくても閲読に支障はないため、だいぶ現代的な書き方になっている。

本当にトラッキングしてないの?

インスペクタ開いてネットワークタブを確認すれば確かめることができる。

(拡張機能を入れている場合、拡張機能の通信に惑わされないように注意)