You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
手段 (既存トークンで通るものを使う): 通知の取得は原則「既読にしない」(復帰補填 / ポーリングモードの定期取得 / AI capability / HTTP API)。可視になった通知カラムが行う取得だけ「既読にする」 (= (a)、権限不要)。可視中の到着は WS の readNotification (= (b)、本家 Web と同じ、権限不要。ポーリングモードでは次の可視中の取得が既読にする)。明示の全部既読は write:notifications が要るので MiAuth の要求権限に足す。既存口座は再認可するまでこの操作を出さず、状態行の最小形 (段階 A で作る) で「再ログインが要る」を案内する。CLI は既定を据え置きフラグで選べるようにする
理由: 今は復帰時の補填やポーリングで取得するたびに全部既読になる。見ていないものが既読になり、本家 Web や他端末の未読も消え、ワーカーの「未読の分だけ出す」(§4.2) も成立しない
本家 Web との差: 本家 Web はウィンドウが可視なら到着時に既読にする。NoteDeck は「通知カラムを見たとき」にする。副作用: 通知カラムを置いていない / 画面外に置いているデスクトップ利用者は、本家 Web や他端末の未読バッジが通知カラムを見るまで残る。それは「見ていない」の正しい表示として受け入れる
バッジの未読数は i から取るように直す (本家に無いエンドポイントを呼んでいる現状の修正)
3.3 OS 通知の出し分け
アプリの状態
ストリーミング経路
背面経路 (ワーカー / push)
前面
その口座の通知カラムが可視なら出さない (メッセージはその会話が可視なら出さない)。それ以外は出す (Android では OS 通知がアプリ内バナーの代わり。音もバッジも通知カラムが mount 中しか鳴らないので、一律抑止すると気づけない)
deep link に口座を載せる (?account=<Misskey ユーザー ID>)。付いていて、そのホストにトークンを持つ口座として実在しなければ開かず状態行へ誘導する (先頭の口座で開くと別口座から返信する事故に戻る)。付いていない既存リンク (コマンドパレット / AI 生成) は先頭の口座のまま
メッセージの会話単位の deep link を新設する。Android の cold start で deep link が効くことを実機項目に入れる
根拠: feat: 通知の制御設定 (ON/OFF・種別・DND・音量) #747 の「通知設定は OS に委ねる」を実現するには OS 側に委ねられる粒度が要る。サーバー側の受信設定はアプリ内表示ごと消す設定なので代替にならない。login は本人確認の性格があり、多発する実績と同じチャネルだと実績を切った人がログイン警告も失う。種別 1 つずつには分けない。チャネル ID は固定し、旧チャネルは更新時に削除する。グループサマリは作らない
4.5 利用者に見せるもの
専用の設定画面・トグルは作らない
通知の権限要求は起動時ではなく、オンボーディング (feat: 初回起動のチュートリアル、初期プロファイルに基本カラムセット #680) の最初の口座追加の直後に組み込む。全 Android 共通で、権限ダイアログの前に説明 1 画面: 「開いている間はすぐ届きます。閉じている間は一定時間ごとに確認します (省電力中は遅れることがあります)」(周期は定数から補間)。拒否が重なって OS がダイアログを出さなくなった後は、状態行から OS の通知設定へ飛べるようにする
Activity
[-]feat: Android バックグラウンド通知(WebSocket フォアグラウンドサービス)[/-][+]feat: Android バックグラウンド通知(WebPush & UnifiedPush)[/+]hitalin commentedon Apr 24, 2026
設計メモ: UnifiedPush + Misskey Web Push (2026-04-24 調査)
WebSocket foreground service 方式から UnifiedPush + Misskey 既存 Web Push 方式に方針転換する提案。
核心
sw/registerは任意 HTTPS endpoint を受け付ける(ドメイン検証なし、本家コード確認済) → サーバー改修不要fcmflavor は FCM 同梱、nofcmflavor(F-Droid向け) は ntfy/Conversations 等の UP distributor 経由 → Google 依存をフレーバー単位で切り替え可能WebSocket foreground service を採らない理由
アーキテクチャ
設計原則: Kotlin 直書き、独自 Tauri plugin を作らない、Rust↔Kotlin RPC なし
理由:
ファイル構成
追加する Kotlin ファイル(全て `src-tauri/android/push/` 配下):
Rust 側は `commands/mod.rs` の `export_account_list()` を拡張して `push_enabled` と `vapid_public_key` を含めるだけ(数行)。
Gradle product flavor:
productFlavors { create(\"fcm\") { dimension = \"store\" } // embedded-fcm-distributor 同梱 create(\"nofcm\") { dimension = \"store\" applicationIdSuffix = \".fdroid\" // distributor 必須、UP のみ } }フェーズ分け
リスク
代替案との比較
参考: Milktea は独自 bridge server (PushToFCM) を運用しているが、2020 年当時は UnifiedPush v3 が未成熟だったため。2026 年時点では不要。
hitalin commentedon Jul 3, 2026
整理: v1.4.3 で Android の WebSocket ストリーミングが動作するようになったため、アプリ起動中のリアルタイム通知は復活済み。本イシューの残スコープは「アプリを閉じている間のバックグラウンド通知」のみで、現行の 15 分ポーリング (WorkManager) を UnifiedPush に置き換える話。設計は上のコメントのまま有効。着手時は Phase 0 の実機 PoC (UnifiedPush receiver と Tauri WebView の共存確認) から。
hitalin commentedon Jul 25, 2026
設計の再検証と更新 (2026-07-25)
着手前に上の設計メモ (2026-04-24) を一次ソースで再検証した。方向性は今も有効だが、前提が古くなっている点と仕様の欠落があったので全面的に更新する。
今も正しいこと
古くなっていた点
PushService(Service) を継承する形が公式の実装形。エンドポイント通知時に URL と Web Push 公開鍵ペア (auth / p256dh) がそのまま渡され、Misskey 側が要求する 3 フィールドに 1:1 で対応する→ 上のメモにあったエンドポイント受け渡し用の FileObserver とアカウント一覧ファイルのスキーマ拡張は不要
通知経路を 2 本に統一し、ポーリングは廃止する
現行のポーリングは Kotlin がアクセストークンを OS のキーストアから直接復号している。これはアプリ未起動でポーリングするための妥協で、この設計で最も筋の悪い部分。
push 方式では Kotlin はトークンに一切触らない (購読登録は Rust 側、受信する payload は復号済み)。つまりポーリングを残す限りこの妥協だけが残り続ける。ポーリングを Rust 側に移すことはできない — 定期実行のワーカーはプロセスこそ起動するがアプリ本体のランタイムを持たないため、残す = トークン復号を Kotlin に残す、と同義になる。
→ push の動作を確認した上でポーリングを廃止する。
トレードオフとして、distributor を導入していない端末やサーバーが Web Push を無効にしているインスタンスではアプリを閉じている間の通知が無くなる。ただし現行のポーリングも Doze 中は端末の maintenance window に従うため、実際には数十分から数時間遅れの通知であり、通知としての価値は既に低い。β 段階で後方互換を残さない方針とも整合する。
なおストリーミングを push に一本化する案は採らない。ストリーミングはタイムライン更新も担っていて通知専用の経路ではなく、アプリ起動中に外部の配信基盤を経由させるのは遅延も無駄も増えるため。供給源は 2 本のまま、描画を 1 箇所に統一するのが妥当な落とし所。
仕様として押さえるべき点
A. 重複の抑止は「フラグ 1 つ」で完結する
フラグはアプリと同一プロセス上の静的な状態として持つ。プロセスが消えれば自動的に倒れるため、「アプリが死んでいるか」を別途判定する必要がない。
この形にすると「プロセスは生きているが接続が切れている」場合 (ネットワーク断、Doze でのソケット切断) にもフラグが倒れているので push が描画し、通知を取りこぼさない。共有設定ファイルにフラグを置くと死亡時に古い値が残るため、静的な状態であることが要件。
B. 通知 ID の採番を Misskey の通知 ID 由来に統一する
現在のストリーミング経路は連番で採番しており、Misskey の通知 ID による重複排除が Android の通知 ID に反映されていない。両経路が同じ通知を拾った場合に 2 件表示される。ID を統一すれば後着が上書きするだけで済み、A の抑止が漏れた場合の保険になる。
C. 通知描画を 1 箇所に統一する
現在のリッチ通知描画は Activity のインスタンスメソッドで、アプリ未起動時には呼べない。application context ベースの静的な描画ヘルパーに切り出し、ストリーミングと push の両経路が同じ描画を共有する。Activity 側のメソッドは残して中身を委譲するだけにすれば、Rust 側の呼び出しは一切変更不要。
D. エンドポイントの取得は非同期であることを前提にする
配信基盤への登録要求を出しても、エンドポイントはその場では返らない (数秒後に非同期で届く)。「登録を指示した直後に取りに行く」設計は成立しない。
→ 初回とホスト追加時のみ、Rust 側が登録指示後に短時間だけ取得をリトライする (数十秒でタイムアウトし、次回起動でリトライ)。あわせて VAPID 公開鍵を Kotlin 側にキャッシュしておき、2 回目以降は起動時に Kotlin が自律的に再登録する。この場合エンドポイントは起動直後に揃うので、Rust は読むだけでよい。
E. ログアウト時の順序制約
アクセストークンを破棄する前に、認証付きで購読解除を済ませる必要がある。解除エンドポイントは認証が任意で、未認証で呼ぶとユーザーで絞られず同じエンドポイントに紐づく全アカウントの購読が消える — ホスト単位でエンドポイントを共有する設計では、1 アカウントのログアウトが同ホストの他アカウントの通知を止める事故になる。逆に解除し損ねるとエンドポイントは失効しないため通知が届き続け、こちらも自然には直らない。
F. 通知のグルーピング方針を決める
ストリーミング経路は「Android は通知チャンネル側でまとまるため集約しない」としている一方、廃止するポーリング経路は独自にグループサマリを作っていた。描画を統一する際にどちらの方針で行くか決める。
制約
配信基盤 (distributor) の扱い
同梱しない。外部の distributor に委ねる。
専用の設定画面は作らない。通知の制御設定をアプリに持たない方針 (#747) を維持し、状態表示が要る場合は既存の「接続」に 1 行置くに留める。有効化トグルも設けない — distributor を導入している事実が実質的なオプトインであり、止めたい場合は OS の通知設定で止められる。
設計の詳細
購読の単位は「ホスト単位」
VAPID 鍵のローテーション検知
既読の同期 (任意)
チャット
責務分割 — 購読登録は Rust 側
理由: アクセストークンは OS キーチェーンが正規の経路であり、購読登録はアプリ起動中に済ませられるため、Kotlin にトークンを触らせる理由がない。複数アカウント分の登録状態管理はロジックが厚く、型付きの層に置いた方が保守しやすい。
トレードオフは、アプリ未起動中にエンドポイントが更新された場合に次回起動まで通知が届かない空白期間が生じること。ローテーションは稀なため許容する。
なお、購読登録に必要なサーバーの VAPID 公開鍵は既存のサーバーメタ取得コマンドがそのまま使える (生 JSON を返すため)。notecli 側の改修は不要。
フェーズ
旧 Phase 4 (product flavors) は同梱をやめたため削除。Phase 1 を先行リファクタとして独立させた。
検証
push は E2E ハーネス (#702) では検証できないため、手動の確認項目を成果物に含める: distributor の有無、アプリの前面 / 背面 / プロセス死、接続が切れている状態、複数アカウント、同一ホストの複数アカウント、ログアウト、Web Push 未設定のインスタンス。
hitalin commentedon Oct 22, 2026
Android の通知 — 設計の全面改訂 (2026-10-22)
2026-07-25 の設計を、本家ソースと現行コードを読み直し、4 視点 (セキュリティ / Android と本家仕様 / アーキテクチャ / 製品価値) の敵対的レビューを 3 巡通して書き直した。採用理由と不採用理由を両方残す。
用語: ワーカー = アプリ終了中に OS が定期的に起こすポーリングのジョブ (今の Android 版にある仕組み)。背面経路 = アプリが前面に無いときに通知を届ける経路 (ワーカー、push)。三つ組 = push 購読の endpoint URL + auth + 公開鍵 (p256dh)。処理位置 = 端末側が口座 × 種別ごとに持つ「最後に処理した通知 ID」(サーバー側の既読位置とは別物)。状態行 = 「接続」設定の各サーバー行に出す背面経路の状態 1 行。
0. ブロッカー: 本家の Web Push 購読登録はアプリのトークンでは呼べない
sw/register/sw/show-registration/sw/update-registrationは「ログイン本体のネイティブトークン」限定 (secure)。MiAuth やアプリ発行のトークンは全部アクセストークンなので ACCESS_DENIED になる。2023-12-27 のセキュリティ修正 (GHSA-7pxq-6xx9-xpgm / CVE-2023-52139) で入り、sw/registerへの差分はこの 1 行だけ (権限の一括棚卸し)。NoteDeck の認証は MiAuth のみsw/unregisterだけは認証任意・権限指定なしで、アクセストークンでも未認証でも呼べる (2026-09-30 から三つ組が必須、endpoint は https 必須)採らない回避策
結論
sw/registerをwrite:notifications権限で許す形で、メッセージ本文がread:chat無しで漏れる論点が付くが、それはこの issue の計画には含めない1. 目的と不変条件
目的: Android でアプリを閉じている間・背面にいる間も、Misskey の通知とメッセージの新着が OS 通知として届く。届くまでの遅れは配送経路で決まり、経路の違いを利用者に説明できる。
2. 確認済みの前提 (2026-10-22)
NoteDeck の現状:
notifications/mark-all-as-read) はバッジタップで呼ぶが、アプリの MiAuth はwrite:notificationsを要求していないので権限エラーを握っている。バッジの未読数は 本家に存在しないエンドポイント を呼んでいて、常に失敗を 0 として扱っている本家 (2026-10-12 時点):
iで取れる。通知作成の 2 秒後に既読位置より新しければ「未読あり」のイベントと (購読があれば) push を送るreadNotificationで既読にする (通知ビューを見ていなくても。権限検査なし)Mk:apiは、トークンを省略するとその Web セッションのトークン (ネイティブ) で呼ぶUnifiedPush の確認済みの事実は §5 冒頭に置く。
3. 今すぐ変える: ストリーミングと既読 (前提の変更)
順序は 3.2 → 3.1 (逆だとポーリングモードで全口座の定期取得が既読を進める)。
3.1 通知を配る main チャンネルは、ログイン中の全口座について Rust の口座セッションが張る
3.2 既読にするのは「見たとき」だけ
readNotification(= (b)、本家 Web と同じ、権限不要。ポーリングモードでは次の可視中の取得が既読にする)。明示の全部既読はwrite:notificationsが要るので MiAuth の要求権限に足す。既存口座は再認可するまでこの操作を出さず、状態行の最小形 (段階 A で作る) で「再ログインが要る」を案内する。CLI は既定を据え置きフラグで選べるようにするiから取るように直す (本家に無いエンドポイントを呼んでいる現状の修正)3.3 OS 通知の出し分け
3.4 通知の同一性と消去
?account=<Misskey ユーザー ID>)。付いていて、そのホストにトークンを持つ口座として実在しなければ開かず状態行へ誘導する (先頭の口座で開くと別口座から返信する事故に戻る)。付いていない既存リンク (コマンドパレット / AI 生成) は先頭の口座のまま4. 今すぐ変える: 背面経路の土台
アプリを閉じている間の通知の実体はワーカー。これを不変条件を満たす形に作り直す。push (§5) が入った口座でもワーカーは安全網として残り、push はそれを速くする。
4.1 描画の組み立てを 2 層に分け、どの経路からも同じ関数を通す
4.2 ワーカーを Rust に移す (常に走る安全網)
ワーカー (Kotlin) は前面フラグ・OS 言語・設定ディレクトリを渡して Rust の入口を呼ぶだけになる。Rust 側:
iを 1 回取る。通知は先に未読数を見る: 0 ならその口座の通知を全部消し処理位置を最新に進めて終わり。n > 0 なら処理位置以降を「既読にしない」で取り、新しい方から高々 n 件だけ描画し、処理位置を最新に進める (本家に通知ごとの既読フラグが無いため、他端末で読んだ分を出さないにはこれしかない)iの未読有無が true のときだけ履歴 (ユーザー / ルーム) を取り、未読の会話の最新メッセージのうち処理位置より新しいものを描画し、処理位置を進める。false なら消す4.3 データと保存先
4.4 通知チャネル
loginは本人確認の性格があり、多発する実績と同じチャネルだと実績を切った人がログイン警告も失う。種別 1 つずつには分けない。チャネル ID は固定し、旧チャネルは更新時に削除する。グループサマリは作らない4.5 利用者に見せるもの
5. push (口座ごとのオプトイン、外部 distributor 限定)
§3 / §4 の上に「背面経路がもう 1 本増え、ワーカーは安全網に下がる」形で載せる。描画・同一性・既読・出し分けは共通で、変える箇所は以下だけ。
購読登録は利用者が本家 Web で行う (Aria 方式)
sw/registerを呼ぶ AiScript のコード (口座名の照合つき) を 1 画面に出す: 「コードをコピー」「本家の Scratchpad を開く」「応答を貼る」の 3 操作。Scratchpad のMk:apiは Web セッションのネイティブトークンで呼ばれるので、本家を変えずにネイティブ限定を通るuserIdとendpointが自分のものと一致するか・keyが null でないかを検証してから保存する。一致しなければ保存せず理由を出す (別口座でログインしている / サーバーが Web Push 未設定)sw/unregisterをアプリから直接呼ぶ (認証任意・三つ組必須)。Aria も同じ確認済みの事実 (本家、2026-10-12 時点): push の型は
notification/unreadAntennaNote/readAllNotifications/newChatMessage。notificationは本文が切り詰められノートの投稿者が落ちる (発生元ユーザーは残る)。newChatMessageは切り詰めない。readAllNotificationsは購読時に希望した場合だけ届く。メッセージの push は 3 秒以内に既読なら送らない。購読登録の重複判定は (ユーザー, 三つ組) の一致。応答にサーバーの VAPID 公開鍵が返り、サーバーが Web Push 未設定でも登録は成功して鍵が null (失敗では検知できない)。送信先が 410 を返したときだけ購読を削除する。本家の Service Worker は 1 日より古い push を捨てる。sw/unregisterは 30 回 / 時のレート制限。確認済みの事実 (UnifiedPush connector v3 / 仕様 AND_3): 受信には
decryptedフラグがあり、復号できなかった平文も届く。配送時に distributor はアプリを 5 秒間だけ前面相当に上げる。登録失敗の種別は INTERNAL_ERROR / NETWORK / ACTION_REQUIRED / VAPID_REQUIRED。登録解除の通知はアンインストールでは来ない。三つ組 = 購読の endpoint URL + auth + 公開鍵 (p256dh)。5.1 受け口: UnifiedPush の connector (v3)。distributor は同梱せず外部に委ねる。1 つなら自動選択、複数なら connector の選択機構に委ねる。ゼロならワーカーのまま。
5.2 責務分担
Kotlin → Rust の経路は JS を経由しない。入口は「ワーカーの実行」「エンドポイントが届いた (合図だけ、秘密は載せない)」「push が届いた (復号済み payload)」。独自の Tauri plugin は作らない: 必要な経路はこれで足り、plugin は Kotlin ソースの置き場と同期方法 (#755) を変える。
5.3 購読の単位とライフサイクル
sw/unregisterで消す5.4 push の中身の扱い
notificationnewChatMessagereadAllNotificationsunreadAntennaNote5.5 セキュリティ
6. 段階
write:notificationsの追加・再認可案内の最小形・バッジの未読数の修正を含む) → ストリーミングの変更 (§3.1)、deep link に口座とメッセージ会話 (§3.4)、プライバシーポリシー検証 (手動):
7. 決めたこと (2026-10-22)
loginは「アカウント」)8. 2026-07-25 の設計からの変更点
feat(notifications): 既読を「通知カラムを見たとき」だけにする (#330)
24 remaining items
docs: push の受け取り方・プライバシー・構成を書く (#330 段階 F)
hitalin commentedon Oct 24, 2026
進捗: 段階 C の残りと段階 F (push) — develop 済み (2026-10-24、未リリース・実機未確認)
段階 C の残り
段階 F: push (口座ごとのオプトイン)
利用者の操作は、アカウントメニューの「プッシュ通知」から 1 画面で完結する。
確かめたこと / まだのこと
fix: Release PR #1280 のレビュー指摘を直す (#330)
hitalin commentedon Oct 24, 2026
段階 F の登録手順を作り直す (仕様、2026-10-24)
動かせない前提
sw/registerは Web UI にサインインしたときの本人用トークンでしか通らない。NoteDeck のトークン、NoteDeck の AiScript、本家の Play (資格なしで呼ぶ)、本家 Web のプラグイン (プラグイン用トークン) はどれも通らない利用者の操作
秘密をコードに入れない (レジストリ + 合言葉)
コードの判定 (第三者の割り込みを止める)
他のアプリも自分の領域に偽の登録待ちを置けるので、登録に進むのは条件がぴったり合うときだけ。
配送アプリへの登録は口座ごと・世代ごと
確定 (届いた push で)
事前判定と失敗の文言
状態と後始末
v1.94.0 からの移行
次の段階 (別 issue)
採らない案
feat(notecli): レジストリの読み書きとテスト通知の API を足す (#330)
feat(push): 登録手順を作り直し、秘密をコードから抜いて届いた push で確定する (#330)
test(push): コードを AiScript 0.19 でも動かし、状態の遷移を表で固定する (#330)
fix(push): アカウントメニューと登録の窓の見た目と導線をほかの項目に揃える (#330)
docs(privacy): レジストリに一時的に書く値に auth (共有の秘密) も含むことを書く (#330)
fix(deck): デスクトップのアカウントメニューから「通知は開いている間に届きます」を外す (#330)
fix(deck): アカウントメニューを開いた時点で push と通知の許可を読む (#330)
fix(android): Rust から JNI で呼ぶ Kotlin のメソッドを release の R8 から守る (#330)
feat(push): push の設定を設定メニューの 1 つの窓にし、アプリはカード・アカウントは行で選ぶ (#330)