コミュニティメンバー表示ポリシーUXモック

コミュニティメンバー表示ポリシー — UXモック

メンバー一覧を「チャンネルに権限のある全員表示」に改め、匿名デフォルト名と参加認知モーダルをセットで導入する案。
コース受講で自動参加する現行モデルは変えない。

現状の仕様
  • 受講生に見えるメンバー一覧・メンション候補は「表示名(display_name)を設定した受講生」+講師+自分のみ
  • チャンネルの権限(コース受講権)があっても、表示名が未設定なら一覧に存在しない
  • これは表示名未設定時に本名・メールへフォールバックして露出するのを防ぐ暫定フィルタで、下記3つの問題を生んでいる
問題1:メンバーが見えない・メンションできない
  • 権限はあるのに一覧に出ない受講生がいて、誰と同じ場にいるのか分からない
  • メンション候補にも出ず、メンバー詳細の取得は404
  • 人数データ(channel_members_count)は全員を数える定義のため一覧と食い違う(合計を表示した時点で不一致が露見)
問題2:本名・メールの露出
  • account.name には本名のほか Stripe 連携等でメールアドレスが入っている場合がある
  • display_name 自体にメールを設定しているケースもある
  • 投稿者名・メンションでは現在もそのまま露出している
問題3:参加の認知がない
  • コース受講と同時に自動でコミュニティメンバーになる
  • 参加した認識のないまま他メンバーから見える状態になっている
変更方針

メンバー一覧を「各チャンネルにアクセス権のあるメンバーは全員表示」に変更する。
それを安全に行うために、下記①〜⑤をセットで導入する(①だけ先行すると問題2が悪化するため分離不可)。

1 権限ベースの全員表示:display_name の有無による除外フィルタを撤廃
2 匿名デフォルト名:未設定・メール形式の表示名は「メンバー + 6桁コード」で統一表示(本名・メールへのフォールバック廃止)。コードはアカウントIDを base36 表記にしたもので、ハッシュではないため衝突しない。設定する表示名は同一コミュニティ内で重複不可(匿名名と同じ形式も予約語として拒否)
3 講師にだけ本名・メール併記admin は全受講生staff は担当コースの受講生のみ。受講生向けレスポンスには併記フィールドを一切含めない(本文に入るラベルは共通表示名のまま)
4 参加の認知:「どこに・なぜ・どう見えるか」を1枚のモーダルで(非ブロッキング・未設定の間は1日1回・状態で文言出し分け)
5 常設導線:「あとで」後も表示名未設定の間は控えめな設定導線を常設

現状 → 変更後の対応表(どの問題がどう解消されるか)

  • メンバー一覧 API はチャンネル単位の権限スコープをすでに持っている(会員公開チャンネル=コミュニティ会員 / コース限定チャンネル=該当コースの受講生+担当講師・幹事等)
  • 今回の要点は、受講生視点だけにある「表示名未設定の受講生を除外するフィルタ」の撤廃と、それを安全にする匿名デフォルト名の導入
項目現状変更後解消する問題
一覧に出る人受講生視点 表示名を設定した受講生+講師+自分のみ チャンネルに権限のある全員(未設定者は匿名IDで表示) 問題1
人数カウント channel_members_count は全員を数える定義で一覧と食い違う(現UIは合計未表示のため露見していない) カウント定義を「権限があり一度でも開いた人」に統一し、一覧+畳み行と常に一致させる。
@channel / @here の送信前ダイアログに出す人数はこれとは別の「通知対象数」(05 参照)
問題1
メンション候補受講生視点 表示名を設定した受講生しか候補に出ない 権限のある全員が候補に出る(匿名IDで挿入 問題1
メンバー詳細の取得 表示名未設定の相手は取得できない(404) 匿名IDのプロフィールとして取得できる 問題1
名前の表示 display_name があればそれを表示し、無ければ name にフォールバック(本名・メールが露出しうる)。メール形式の display_name もそのまま表示する display_name(メール形式を除く)があればそれを表示し、無ければ匿名ID。本名・メールに依存しないフォールバックにする 問題2
参加の認知 なし(受講と同時に無通知でメンバー化) 参加認知モーダル(表示名未設定の間は1日1回)+未設定の間の常設導線 問題3
メンションの解決今回は解消しない 本文の @表示名 を文字列一致で解決している 変更しない。既存の重複表示名と、表示名変更前に書かれた過去メッセージでは曖昧性が残る(05 参照) 将来対応
ポイント ——「フィルタを消すだけ」の変更ではない
  • 変更しない:権限スコープ・投稿権限・アクセス可否の判定
  • 新規実装1:講師向けの本名・メール併記(serializer への新規フィールド。現状、他メンバーの email を独立フィールドで返す community API は無い)
  • 新規実装2:人数カウント定義の統一
  • 新規実装3:未オープン/長期未利用フィルタ
  • 新規実装4:メンバー検索と最終利用順ソート(一覧の limit+カーソルページング自体は実装済み。カーソルキーの差し替えで対応)
  • 新規実装5:表示名の重複・予約語バリデーション(現状は長さ制限のみで一意制約がなく、全員表示にすると同名が並んで誰が誰か分からなくなる)
  • 新規実装6:登録メールと同一の display_name を NULL 化する migration(リリース時に一度だけ実行)

参加認知フロー(今回の拡張)

確認するのは2つ。いつ・何が起きるか(下の4ステップ)と、誰にどのモーダルが出るか(出し分け表)。実装の詳細は 05 実装・仕様メモ に分けてある。

  • アクセス権のモデルは変えない(コース受講権からの導出のまま・参加ボタンでのゲートはしない)
  • 「勝手に参加している」感覚を初回の認知見え方の透明化で解消する

いつ・何が起きるか

1コース受講権の付与
  • 招待URL・決済・講師登録など既存の導線のまま
  • ここでは何も出さない(コミュニティを使わない受講生へのノイズを避けるため)
2コミュニティを初めて開く
  • 最終オープン日時を community_last_opened_at に記録する
  • NULL=未オープンでモーダル対象かつ一覧非表示
  • 以後はオープンごとに touch する
3参加認知モーダル未設定なら1日1回
  • どこに(コミュニティ名)・なぜ(受講中コース)・どう見えているか(匿名ID)を伝える
  • 表示名の設定を促すが、閉じても利用は妨げない
  • 表示名が未設定(メール形式を含む)の間は、1日の最初のオープンで出す(下の表)
4「あとで」の後
  • その日は再表示しない
  • 未設定のままなら翌日の最初のオープンでまた出す
  • 左下の自分アイコンとメンバー一覧の自分行に控えめな設定導線を常設する

誰にどのモーダルが出るか

  • 初回オープン(パターン1)は display_name の状態によらず必ず出す(参加の認知が目的のため)。2回目以降は display_name の状態で決まる(未設定・メール形式は出す、設定済みは出さない)
  • 出す場合の頻度は「表示名が未設定の間、1日の最初のオープンで1回」。設定した時点で止まる
対象いつ出るか見出し内容の重心
1新規メンバー コミュニティ初回オープン時 「◯◯のコミュニティへようこそ」 参加の認知(どこに・なぜ参加しているか)+見え方+表示名設定
2既存・表示名 未設定 リリース後、次に開いた時 「名前の表示方法が変わりました」 表示ポリシー変更の告知。「ようこそ」は使わない(すでに使っている人に不自然なため)
3既存・表示名 設定済みメール形式でない 出さない 見え方が変わらないため告知しない。変更したくなったら常設導線から設定する
4既存・メール表示名手入力のメール形式 リリース後、次に開いた時 パターン2 と同じ+注意文 パターン2 に加えて「メールアドレスは表示名として使えなくなった」ことを明示。登録メールと同一だった人は migration で NULL 化されパターン2 に入る
52日目以降のリマインド1・2・4 を閉じて未設定のまま 翌日以降、その日の最初のオープン(1日1回) 「表示名を設定しませんか」 告知は繰り返さず設定を促すだけに絞る。設定した時点で出なくなる

見え方マトリクス(確認済み仕様)

  • 本名・メールの併記は講師の権限で範囲が変わるadmin は全受講生に併記、staff は自分が担当するコースに権限のある受講生のみに併記する(staff の担当コースは group 経由で解決する既存の仕組みをそのまま使う)
  • 受講生向けレスポンスには併記フィールドを一切含めない(値を空にするのではなく、キー自体を返さない)。staff から見た担当外の受講生も同じ扱いにする
相手の状態受講生視点講師視点admin(全受講生)/staff(担当コース内)講師視点staff(担当コース外)
display_name 設定済みメールを含まない メンバー一覧・メンションとも display_name display_name + account.name・user.email を併記(一覧・メンション候補とも) display_name のみ(受講生と同じ・併記なし)
display_name 設定済みメールを含む(手入力) 匿名ID(display_name は表示しない=露出解消) 匿名ID + account.name・user.email を併記 +「メール表示名」チップ 匿名ID のみ(受講生と同じ・併記なし)
display_name 未設定 匿名ID 匿名ID + account.name・user.email を併記 +「未設定」チップ 匿名ID のみ(受講生と同じ・併記なし)

この変更で満たす要件

一覧・メンションに個人情報を含めない 受講生は他受講生のメール・本名を見えない 講師はメール・本名・表示名を見える(admin は全受講生 / staff は担当コースの受講生) name / display_name へのメール混入の露出を解消 一覧+畳み表記が人数カウントと常に一致(未オープンは非表示・長期未利用は除外) どこに・なぜ参加しているかが分かる(参加認知モーダル) 自分がどう見えているかが分かる

実装・仕様メモ

01〜03 で決めた方針を、実装時に迷わない粒度まで落としたもの。テーマごとに分けてあるので、必要なところだけ読めば足りる。

1表示名と匿名デフォルト名のルール

匿名IDをどう作るか。保存時に何を弾くか(@ 混入・重複・予約語)と、その判定方法。

仕様匿名デフォルト名の生成規則
  • 形式は「メンバー」+6桁のコード(例 メンバー0009ix
  • コードは account.id を base36(0-9a-z)表記にしてゼロ埋めしたもの。ハッシュではないため、桁数に関係なく衝突は原理的に起きない
  • 36⁶ = 約21.7億まで6桁で表現できる。これを超えた場合は桁が増えるだけで一意性は保たれる(ゼロ埋めは「最低6桁」の意味)
  • account はコミュニティ(enterprise)ごとに別レコードのため、同じ人でもコミュニティが違えば別の匿名IDになる
  • アカウントの公開IDはすでに API 上で可逆な形で露出しており、base36 化で新たに漏れる情報はない。匿名ID同士を見比べれば登録の前後関係はおおよそ推測できるが許容する
  • この「メンバー」+6桁の形式は表示名の予約語として拒否する(→ 右の重複チェック)
実装メモ表示名の保存・バリデーション
  • 保存は既存の /v2/community/profile を利用(新規 endpoint は不要)
  • 現状の validation は Account#display_name / EnterpriseManager#display_name とも長さ(最大100)のみ。一意制約も unique index も無い
  • 追加が必要なのは @ 混入 / 重複 / 匿名名パターン(予約語) の3つ
  • 失敗は 422 + Problem Detailsremediation: FIX_INPUT)で返し、フロントはフォーム inline に出す(toast にしない)
仕様表示名の重複チェック
  • 比較範囲は同一コミュニティ(enterprise)内。表示名は enterprise ごとの account 単位で独立なので、他コミュニティの表示名とは衝突させない
  • 受講生(accounts)と講師(enterprise_managers)は同じ名前空間で見る(同じ一覧に並ぶうえ、講師へのなりすましが最も影響が大きいため)
  • 比較キーは前後空白除去 → 連続空白を1つ → NFKC(全角/半角)→ 大文字小文字を無視。「Rika」「rika」「Rika 」は同一扱い
  • 匿名デフォルト名と同じ「メンバー」+6桁の形式は予約語として拒否(他人の匿名IDを名乗れないようにする)
  • 未設定・メール形式の display_name は比較対象外(どちらも匿名IDで表示されるため)。論理削除済みのアカウントも除外する
  • 既存の重複は移行時に強制変更しない。保存時のみ検査する(既存メンバーの表示名を一括で書き換える方が混乱が大きい)
実装メモ重複チェックを DB 制約にしない理由
  • スコープが accountsenterprise_managers2テーブル横断で、比較も正規化キーのため、単一の unique index では表現できない
  • そのためアプリ側 validation とし、同時保存によるすれ違いは許容する(発生頻度が低く、影響も表示名の重複に留まるため)
  • 厳密にしたい場合は、正規化キーを持つ専用テーブル(enterprise_id + 正規化キーに unique index)を別途検討する
  • 検索は正規化キーでの比較になるため、該当カラムへの index だけでは効かない点に注意(コミュニティ内の件数は限られるので初期は許容)

2メンバー一覧の表示範囲

誰が一覧に出るか・出ないか。02 の対応表を実装レベルで補足する。

実装メモメンバー一覧 API の変更点
  • チャンネル権限でのスコープ(全員公開 / コース限定)は既存のまま
  • 変更の実体は受講生視点の「表示名未設定の除外フィルタ」の撤廃(個別取得の 404・メンション候補の除外も同時に撤廃)
  • 加えて最終オープン状態でのフィルタ(NULL は非表示・1ヶ月以上は畳み。受講生視点の一覧のみ)
  • 一覧の分割取得(limit+カーソル)は実装済みのため追加不要。最終利用順への変更に伴うカーソルキーの差し替えのみ行う
  • 匿名デフォルト名と同一リリースで行う(撤廃だけ先行すると本名・メールの露出が拡大するため)
仕様フィルタの適用範囲
  • メンション候補・講師の DM 宛先には適用しない(宛先から消えると連絡手段が失われるため)
  • 畳み・非表示はメンバー一覧の表示だけに閉じる
仕様長期未利用・未オープンの扱い
  • 1ヶ月以上未利用:オフライン欄で畳み表示(人数のみ)
  • 一度も開いていない人:一覧・畳み行・カウントすべてに含めない
  • 講師・自分は対象外で常に表示。受講生視点のみに適用し、講師視点は常に全員表示(名簿として把握が必要なため)
  • 人数カウントは「権限があり一度でも開いた人」に統一 → 一覧+畳み行=カウントで不一致を再発させない
  • 「1ヶ月」の閾値は仮の値で要調整
仕様人数カウントと @channel の通知対象は別指標
  • メンバーパネルの人数=「権限があり一度でも開いた人」。見えている一覧+畳み行と一致することを優先する
  • @channel / @everyone / @here の送信前ダイアログの人数通知対象数。メンション候補(=チャンネルに権限のある全員)を数える。@here はそのうちオンラインの人数
  • 送信前ダイアログはすでに実装済みで、この2つは元から別ソース。メンション候補には未オープン/長期未利用フィルタを掛けないため、フィルタ撤廃後は「候補数=@channel の通知対象」で一致する
  • 現状はメンション候補が「表示名を設定した受講生のみ」なので、ダイアログの人数が実際の通知対象より少ない。今回の変更でむしろ正確になる
  • @here だけは現状ズレている:ダイアログの人数がメンバーパネルのオンライン人数(=自分を含む・フィルタ後)から来ており、実際の通知対象(オンラインの通知対象・送信者は除く)とは別データ。通知対象数に揃える
  • 2つの数字が比較されないよう文言で役割を分ける(パネル=「メンバー ●人」/ダイアログ=「このチャンネルの ●人に通知されます」)
  • 実際の見え方は 04 画面モックの「@channel の確認」「@here の確認」で、現状/変更後を切り替えて確認できる

3参加認知モーダル

03 のフローを支える新設カラムと判定ロジック。

実装メモcommunity_last_opened_at(新設カラム)
  • モーダルの初回判定と最終利用の記録を兼用(enterprise ごとの account 単位)
  • NULL=未オープン → モーダル対象かつ一覧非表示。オープンごとに touch(1日1回程度に間引き)
  • 既存の chat_reads / chats を一覧クエリで JOIN する案は不採用(ROM 専の受講生を未利用扱いにする・JOIN と index が重い)
  • リリース時は migration で chat_reads.updated_at の MAX 等から一括 backfill。値があれば既存 / NULL のままなら新規として、モーダル出し分けの判定にも使う
実装メモ「1日1回」の判定
  • 表示条件は 表示名が未設定(または @ を含む) かつ その日まだ開いていない
  • 「その日まだ開いていない」は touch 前の community_last_opened_at の日付 ≠ 今日 で判定できる(専用カラムは増やさない)
  • touch を「1日1回程度に間引く」実装と判定タイミングが競合しないよう、オープン時は先に判定→後で touch の順にする
  • 日付境界はユーザーのタイムゾーンで判断する(サーバー TZ 固定だと深夜帯にずれる)
仕様複数コース参加時のモーダル文言
  • 参加理由は「◯◯」ほか2件と畳んで表示する(先頭1件+残り件数)
  • 1コースのみなら「◯◯」だけ。コース名を全件並べることはしない
  • 04 の画面モックの参加認知モーダルに反映済み

4本名・メール併記の範囲

誰に併記するか(admin / staff)と、どこに出すか。

仕様併記するロールと範囲
  • admin:コミュニティの全受講生に本名(account.name)とメール(user.email)を併記する
  • staff自分が担当するコースに権限のある受講生のみ併記する。担当外の受講生は受講生と同じ見え方(匿名ID または display_name のみ)
  • 受講生向けレスポンスには併記フィールドを一切含めない。値を空にするのではなくキー自体を返さない
  • 併記が出るのはメンバー一覧・メンバー詳細・メンション候補・DM 宛先の表示のみ。メッセージ本文に入るラベルは共通の表示名のまま(本文は全員が同じものを見るため)
実装メモstaff の担当コース判定
  • 担当コースは既存の group 経由の担当割当をそのまま使う(Enterprise#staffs(courses) と同じ関係)
  • チャンネル単位ではなく受講生1行ごとの判定になる点に注意。全員公開チャンネルには staff の担当外受講生も並ぶ
  • N+1 を避けるため、staff の担当コースID集合を1回だけ引き、各受講生の受講コースID集合との交差で判定する
  • serializer は viewer の権限でフィールドの有無を切り替える(null を返すのではなく、キーを出さない)

5今回解消しない制約と将来対応

許容する残課題と、その根本解決をどこに置くか。

仕様メンションの曖昧性が残るケース
  • 既存の重複表示名:移行時に強制変更しないため、同名のまま残る組み合わせがある
  • 表示名の変更・後からの設定:過去メッセージの本文に文字列として残った旧名・旧匿名IDは、新しい表示名には解決されない
  • どちらも許容する。既存メンバーの表示名を一括で書き換える方が混乱が大きく、過去本文の書き換えも実質できないため
実装メモ具体的にどう壊れるか
  • 本文のメンションは @表示名 の文字列一致で対象を決めている(送信時にメンション候補と突き合わせて対象IDを付ける)
  • 同名が2人いると両方が対象になり、意図しない相手にも通知が飛ぶ
  • 表示とプロフィール解決は最初に一致した1人になるため、誤ハイライトが起きうる
実装メモ漸減と将来対応
  • 新規の重複はバリデーションで発生しない。既存の重複もその人が表示名を編集した時点で解消されるため、時間とともに減る
  • 根本解決は本文のメンションをIDトークンで保存し(例 <@ID>)、表示時に現在の表示名へ解決する方式。表示名の変更にも追随できる
  • ただし過去本文の再パースとエディタ・本文フォーマットの変更が必要なため、今回のスコープ外(将来対応)とする