Skip to content

スレッド型メッセージング(新しいサイト種別 Threads)の設計 ​

第1版作成 最終更新 (日本時間)
確認バージョン1.5.8.1

本体の標準機能ではありません

このページは本体を改修する場合の設計メモです。前提にした現行の実装は 1.5.8.1 です。

Slack・Mattermost・Rocket.Chat のようなメッセージングを、プリザンターのサイト階層の中に「チャンネル」として置けるようにする設計です。既存のテーブル(期限付き・記録)のコメントを使うのではなく、新しいサイト種別 Threads を足し、メッセージ・スレッド返信・リアクション・メンション・既読を専用のテーブルで持ちます。

前提にした現行の実装 ​

サイト種別 ​

サイトの種類は Sites.ReferenceType の文字列で決まります。

ReferenceType用途特徴
Sitesフォルダ他のサイトを入れる
Issues期限付きテーブル開始・完了・進捗率・工数など
Results記録テーブル状況・管理者・担当者など
WikisWikiサイトに 1 レコード
Dashboardsダッシュボード表示用。レコード操作なし

レコードのモデルは IssueModel・ResultModel・WikiModel・DashboardModel がどれも BaseItemModel(SiteId・Title・Body など)を継承し、その上の BaseModel が Ver・Comments・Creator・Updator・CreatedTime・UpdatedTime を持ちます。

Items テーブルと ID の採番 ​

どの種別のレコードも Items テーブルに 1 行を持ち、ReferenceType と ReferenceId で各テーブルに対応します。ID を採番するのは Items.ReferenceId(Identity)で、各テーブルの ID(IssueId など)は Identity ではありません。作成時は Items に先に INSERT し(selectIdentity: true)、得た ID で Issues に INSERT します(IssueModel.cs#L1832-L1860)。

図を読み込み中…

種別ごとの振り分け ​

ItemModel は Site.ReferenceType の switch で各 *Utilities に処理を振り分けます。1.5.8.1 の ItemModel.cs には switch (Site.ReferenceType) が 82 か所あります。

csharp
switch (Site.ReferenceType)
{
    case "Sites":
        return SiteUtilities.SiteMenu(context: context, ss: ss);
    case "Dashboards":
        return DashboardUtilities.Index(context: context, ss: ss);
    case "Issues":
        return IssueUtilities.Index(context: context, ss: ss);
    case "Results":
        return ResultUtilities.Index(context: context, ss: ss);
    // ...
}

モデル・SQL・Validators の多くは CodeDefiner が Definition_Column・Definition_Code・Definition_Sql の定義から生成します(CodeDefiner)。

要件 ​

チャットツールの概念は次のように対応付けます。

チャットツールプリザンター
ワークスペーステナント
チームフォルダ
チャンネルReferenceType = "Threads" のサイト
メッセージThreads のレコード(ParentId = 0)
スレッド返信Threads のレコード(ParentId で親を指す)
リアクションThreadReactions テーブル
メンション本文の @ユーザー を解析
添付ファイル既存の Binaries テーブル

Slack・Teams・Mattermost・Chatwork・LINE WORKS の機能を比べ、次の優先度で取り入れます。スレッド返信やキーワード通知は欧米のツール、メッセージごとの既読者一覧は Chatwork・LINE WORKS にある機能です。

優先度機能
必須スレッド返信、メンション通知、既読・未読、未読件数バッジ、リアクション、ファイル添付と画像プレビュー
高メッセージの編集・削除、Markdown、参加したスレッドの自動フォロー通知、全文検索、チャンネルのミュート
中既読者一覧、ピン留め、ブックマーク、キーワード通知、引用返信
低おやすみモード(DND)、検索の絞り込み構文(from: など)、タスク(Issues)との連携
対象外カスタム絵文字、スレッド返信のチャンネルへの転送

テーブル ​

図を読み込み中…

テーブル内容
Threadsメッセージ本体。BaseItemModel の列(SiteId・Title・Body・拡張列の ClassA など)に、ThreadId・ParentId(0 ならルート)・Locked を足す
Threads_history / Threads_deleted他のテーブルと同じ派生テーブル。更新時は更新前を _history に(主キー ThreadId・Ver)、削除時は _deleted に退避する
ThreadReactionsリアクション。主キーは ThreadId・UserId・Reaction
ThreadMentionsメンション。MentionType は user / all / here。UserId・IsRead にインデックス
ThreadReadPositionsチャンネルごと・ユーザーごとの既読位置
  • ThreadId は他の種別と同じく Items.ReferenceId の値を使います。Identity にはしません(上の「ID の採番」のとおり、作成は Items → Threads の順)。
  • インデックスは (SiteId, CreatedTime DESC)(チャンネルの一覧)、(ParentId, CreatedTime)(スレッドの返信)、ParentId = 0 の絞り込みインデックス(ルートだけ)を張ります。
  • ルートメッセージの一覧で返信数を毎回数えると N+1 になるため、ReplyCount・LastReplyTime を Threads に持ち、返信の投稿時に親の値を更新します。
  • スレッドは Slack と同じく 1 階層だけにします。返信先の ParentId が 0 でなければ(返信への返信なら)作成時のバリデーションで拒否します。
  • ルートメッセージを消すときは、ThreadId = @id OR ParentId = @id で返信もまとめて _deleted に移します。復元は _deleted から戻します。

モデル・画面・API ​

足すもの ​

区分ファイル内容
定義Definition_Column/Threads_*.json などThreads の列定義(CodeDefiner で ThreadModel・Rds.Threads*()・派生テーブルを生成)
モデルModels/Threads/(新規)ThreadModel・ThreadCollection・ThreadUtilities・ThreadValidators・ThreadApiModel・ThreadExportModel
モデルModels/Items/ItemModel.cs各 switch に case "Threads":(Index・IndexJson・Editor・Create・Update・Delete・CreateByApi・GetByApi など)
モデルModels/Items/ItemUtilities.csItemJoin に Threads を足す
サイトModels/Sites/SiteUtilities.csサイト作成時の種別の選択肢と表示名に Threads
設定Libraries/Settings/SiteSettings.csリアクション・メンション・返信・Markdown・添付の有効化、最大文字数、ポーリング間隔など
画面Libraries/HtmlParts/HtmlThreads.cs(新規)タイムライン・返信パネル・添付・未読バッジ
画面thread.js・thread.css(新規)投稿・スレッドを開く・既読位置・ドラッグ&ドロップ
通知Libraries/Settings/Notification.csThreads 用の通知の条件
添付BinaryUtilities.cs・BinariesController.csThreads のメッセージへの添付
表示文字列App_Data/Displays/Threads などの表示名

画面 ​

一覧は表ではなくタイムラインにします。メッセージをクリックすると右のパネルでスレッドが開き、そこで返信します。HTML は既存と同じく HtmlBuilder でサーバー側で組み、本文は Markdown として描画します。投稿やスレッドの展開は $p.ajax で行います。

text
+-------------------------------+  +---------------------------+
| #general          [設定][検索] |  | スレッド                  |
| [メッセージ入力欄]    [送信]  |  +---------------------------+
+-------------------------------+  | ユーザーA  03/03 10:30    |
| ユーザーA   2026/03/03 10:30  |  | 進捗を共有します。        |
| 進捗を共有します。  ← 選択    |  +---------------------------+
| 添付: report.pdf              |  | ユーザーB  03/03 10:45    |
| [返信 3件] [👍2]              |  | 確認します。              |
+-------------------------------+  +---------------------------+
| ---- ここから未読 ----        |  | [返信入力欄]    [送信]    |
| ユーザーC   2026/03/03 11:15  |  +---------------------------+
| @ユーザーA レビューお願いします|
+-------------------------------+

API ​

既存の /api/items/{id}/... の形に合わせます。

メソッドURL内容
POST/api/items/{siteId}/create投稿(ParentId を付けると返信)
POST/api/items/{threadId}/update編集
POST/api/items/{threadId}/delete削除
POST/api/items/{siteId}/get一覧(View.ColumnFilterHash で ParentId: "[0]" を指定するとルートだけ)
POST/api/items/{threadId}/threadスレッドの返信一覧
POST / DELETE/api/items/{threadId}/reactリアクションの追加・削除
POST/api/items/{siteId}/readposition既読位置の更新
POST/api/items/{siteId}/unreadcount未読件数
POST/api/items/{threadId}/readby既読者一覧
json
{
    "ApiVersion": 1.1,
    "ApiKey": "...",
    "Title": "新しいトピック",
    "Body": "進捗を共有します。\n@userB 確認お願いします。",
    "ParentId": 0
}

一覧の応答では、各メッセージに ThreadId・ParentId・Title・Body・Creator・CreatorName・CreatedTime・ReplyCount・LastReplyTime・Reactions(Reaction と Count)を返します。

メンションと通知 ​

投稿時に本文から @(\w+) を拾ってログイン ID からユーザーを引き、ThreadMentions に記録します。@all・@here は MentionType で区別します。通知は既存の Notification(1.5.8.1 の種類は Mail・Slack・ChatWork・Line・LineGroup・Teams・RocketChat・InCircle・HttpClient・LineWorks)で送ります(Notification.cs#L51-L63)。

きっかけ通知先
@ユーザーそのユーザー
@all / @hereチャンネルの全員 / オンラインの人
フォロー中・自分が投稿した・メンションされたスレッドへの返信フォローしている人、投稿者、メンションされた人
チャンネルの通知設定が「全メッセージ」その設定の人
登録したキーワードを含むキーワードを登録した人

図を読み込み中…

ユーザーごとのチャンネルの通知設定は NotifyLevel(all / mention(既定)/ none)とミュートで持ちます。

権限 ​

サイトの権限をそのまま使い、編集・削除だけ投稿者本人かどうかを足して確かめます。

操作必要な権限
読む・リアクションする読み取り
投稿する作成
自分のメッセージを編集する更新 + 投稿者本人
他人のメッセージを編集するサイトの管理
削除する削除 + (投稿者本人 または 管理)
チャンネルの設定サイトの管理

ファイル添付 ​

添付は既存の Binaries テーブルに ReferenceId = ThreadId・BinaryType = "Attachments" で入れ、表示・ダウンロードは既存の /binaries/{guid}/show・/binaries/{guid}/download を使います。保存先は BinaryStorage.json の設定に従い、サイズ・件数・拡張子は既存の BinaryValidators の考え方で確かめます(1.5.8.1 で既定の拒否リスト Form.json の AttachmentExcludedExtensions を見るのはフォームの添付だけなので、Threads でも使うなら判定を足す必要があります。添付ファイルの拡張子制限)。入力欄では、ファイル選択・ドラッグ&ドロップ・クリップボードの画像の貼り付けに対応し、画像はサムネイルで出します。メッセージを消すと添付も Binaries_deleted に移します。

既読管理 ​

「そのチャンネルで最後に読んだ ThreadId」だけを ThreadReadPositions に持ちます。ThreadId(= Items.ReferenceId)は増える一方なので、LastReadThreadId より大きいものが未読です。返信も含めてチャンネル全体で 1 つの位置を持ちます(Slack と同じ)。

  • チャンネルを開いたとき・ポーリングで新着を受け取ったときに、表示中の最大の ThreadId で更新します。
  • 未読件数は、ルートメッセージ(ParentId = 0)のうち ThreadId > LastReadThreadId のものを数えます。チャンネル一覧にバッジ(100 以上は 99+)を、タイムラインに「ここから未読」の線を出します。
  • 既読者一覧は、LastReadThreadId >= 対象の ThreadId のユーザーを引けば求まります。
sql
SELECT t."SiteId", COUNT(*) AS "UnreadCount"
FROM "Threads" t
LEFT JOIN "ThreadReadPositions" rp
    ON rp."SiteId" = t."SiteId" AND rp."UserId" = @UserId
WHERE t."SiteId" IN (@SiteIds)
  AND t."ParentId" = 0
  AND t."ThreadId" > COALESCE(rp."LastReadThreadId", 0)
GROUP BY t."SiteId";

リアルタイム更新 ​

方式遅れサーバー負荷実装
Ajax ポーリング(最初はこれ)数秒〜数十秒高い既存の $p.ajax で一覧 API を定期的に呼ぶ。間隔はサイト設定(既定 5 秒、0 で無効)
SignalR(将来)すぐ低いチャンネルごとのグループに新着を送る。1.5.8.1 は SignalR を使っていないので、サーバー側のハブとクライアントのライブラリを足す

既存のコメントとの違い ​

既存のコメントThreads
保存先レコードの Comments 列(JSON 配列)専用テーブル
対象期限付き・記録・Wiki のレコードチャンネル内のメッセージ
専用 APIなし(更新 API で書く)あり
スレッド・リアクション・メンションなしあり
変更の追跡CommentId と更新日時_history / _deleted

コメントとは統合せず、用途で使い分けます。コメントだけを操作する API の改修案は コメントだけを追加・編集・削除する にあります。

関連ページ ​

変更履歴

第1版拡張ライブラリの読み込みと開発・デバッグ、拡張ヘッドリンク、SMTP の OAuth 送信の解説と、多言語・外部公開カレンダー・スレッド型サイトなどの改修・設計メモを追加