スレッド型メッセージング(新しいサイト種別 Threads)の設計
本体の標準機能ではありません
このページは本体を改修する場合の設計メモです。前提にした現行の実装は 1.5.8.1 です。
Slack・Mattermost・Rocket.Chat のようなメッセージングを、プリザンターのサイト階層の中に「チャンネル」として置けるようにする設計です。既存のテーブル(期限付き・記録)のコメントを使うのではなく、新しいサイト種別 Threads を足し、メッセージ・スレッド返信・リアクション・メンション・既読を専用のテーブルで持ちます。
前提にした現行の実装
サイト種別
サイトの種類は Sites.ReferenceType の文字列で決まります。
| ReferenceType | 用途 | 特徴 |
|---|---|---|
Sites | フォルダ | 他のサイトを入れる |
Issues | 期限付きテーブル | 開始・完了・進捗率・工数など |
Results | 記録テーブル | 状況・管理者・担当者など |
Wikis | Wiki | サイトに 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 か所あります。
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.cs | ItemJoin に Threads を足す |
| サイト | Models/Sites/SiteUtilities.cs | サイト作成時の種別の選択肢と表示名に Threads |
| 設定 | Libraries/Settings/SiteSettings.cs | リアクション・メンション・返信・Markdown・添付の有効化、最大文字数、ポーリング間隔など |
| 画面 | Libraries/HtmlParts/HtmlThreads.cs(新規) | タイムライン・返信パネル・添付・未読バッジ |
| 画面 | thread.js・thread.css(新規) | 投稿・スレッドを開く・既読位置・ドラッグ&ドロップ |
| 通知 | Libraries/Settings/Notification.cs | Threads 用の通知の条件 |
| 添付 | BinaryUtilities.cs・BinariesController.cs | Threads のメッセージへの添付 |
| 表示文字列 | App_Data/Displays/ | Threads などの表示名 |
画面
一覧は表ではなくタイムラインにします。メッセージをクリックすると右のパネルでスレッドが開き、そこで返信します。HTML は既存と同じく HtmlBuilder でサーバー側で組み、本文は Markdown として描画します。投稿やスレッドの展開は $p.ajax で行います。
+-------------------------------+ +---------------------------+
| #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 | 既読者一覧 |
{
"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のユーザーを引けば求まります。
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 の改修案は コメントだけを追加・編集・削除する にあります。