Skip to content

feat: soju.im/webpush push notifications#236

Open
ValwareIRC wants to merge 3 commits into
mainfrom
feature/webpush
Open

feat: soju.im/webpush push notifications#236
ValwareIRC wants to merge 3 commits into
mainfrom
feature/webpush

Conversation

@ValwareIRC

@ValwareIRC ValwareIRC commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Client support for the soju.im/webpush IRCv3 extension — wakes the device for DMs and channel highlights via the platform push service even when the tab is closed.

  • Requests the soju.im/webpush capability and reads the VAPID= ISUPPORT token onto server state (server.vapidKey).
  • src/lib/webpush.ts: subscribes the browser PushManager against the server's VAPID key and sends WEBPUSH REGISTER <endpoint> p256dh=…;auth=…; resubscribes if the server's key rotated; unregisterWebPush tears it down + sends WEBPUSH UNREGISTER.
  • Service worker (public/sw.js): push handler parses the one-IRC-line payload and shows a notification — nick in #channel · <network> for highlights, nick · <network> for DMs (network from the manifest). Suppressed when a client window is focused (the in-app notifier covers that). Plus a notificationclick that focuses/opens the app.
  • Settings → Notifications gains an Enable Notifications master toggle (enableNotifications, which previously existed in state but had no UI control). Turning it on is the user-gesture opt-in: prompts for permission and registers push on every connected push-capable server; turning it off unregisters.
  • Auto-registers on ready for servers that already have permission granted.

Server side (obbyircd webpush module + hosted-backend VAPID/encrypt-send) lives in the ObbyIRCd / hosted-backend repos; this PR is the client half.

Test plan

  • Verified end-to-end against obby.t3ks.com: DM and channel highlight both deliver a push (push service returns 201) with the tab closed (account has persistence on so the ghost stays in channels).
  • Enable notifications in Settings → grant permission → confirm a WEBPUSH REGISTER is sent and a subscription row is stored.
  • Confirm the notification title shows the channel + network and that a focused tab suppresses the push (in-app notifier handles it).
  • Toggle notifications off → confirm WEBPUSH UNREGISTER.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Desktop notifications now available for mentions and direct messages
    • Web push support enables notifications even when the app is closed on compatible servers
    • New notification setting in user preferences to enable or disable notifications
    • Improved notification handling with per-channel and direct message tracking

Review Change Stack

Requests the soju.im/webpush cap, reads the VAPID= ISUPPORT token onto
server state, and on a webpush-capable server (with notification
permission granted) subscribes the browser PushManager against that
VAPID key and sends WEBPUSH REGISTER <endpoint> p256dh=..;auth=..

Service worker gains a push handler that parses the one-IRC-line
payload and shows a notification (suppressed when a window is focused)
plus a notificationclick that focuses/opens the app.  Enabling
notifications in settings is the user-gesture opt-in.
…tration

The notifications category had no master enable toggle, and the
enableNotifications GlobalSettings field had no UI control at all -- so
there was no way to opt into push (or in-app notifications). Add a
notifications.enable toggle (key enableNotifications, priority 0);
flipping it on prompts for permission + WEBPUSH REGISTER on push-capable
servers, flipping it off WEBPUSH UNREGISTERs.
The SW only showed the sender nick. Now channel highlights render as
"alice in #weather" and DMs as "alice", with the network name (from
the manifest) appended as context. Notification tag groups by channel
for highlights / by sender for DMs so repeats collapse.
@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR implements a complete web push notification system for ObsidianIRC. It adds a user-facing notification toggle setting, negotiates push support with IRC servers, processes incoming push notifications in the service worker, and manages subscription lifecycle in user settings, with comprehensive localization across 18 languages.

Changes

Web Push Notification System Implementation

Layer / File(s) Summary
Notification setting definition and localization
src/lib/settings/definitions/allSettings.ts, src/locales/{cs,de,en,es,fi,fr,it,ja,ko,nl,pl,pt,ro,ru,sv,tr}/messages.{mjs,po}
New notifications.enable boolean setting with localized UI strings ("Enable Notifications" and behavior description) in 18 language catalogs, using both compiled .mjs modules and translation source .po files.
IRC server capability negotiation
src/lib/irc/IRCClient.ts
Adds soju.im/webpush capability to the client's CAP negotiation list to advertise browser push subscription support to compatible servers.
Service worker push event handling and notification display
public/sw.js
Implements push event parsing (IRC message format: msgid, nick, command, target, text extraction), network name caching from manifest, channel vs. DM classification, focus-aware notification suppression, and notification display with per-conversation tags; adds notificationclick handler to focus existing client windows or open the app.
User settings notification subscription management
src/components/ui/UserSettings.tsx
Extends settings save flow to request notification permission and register/unregister web push subscriptions per server (when soju.im/webpush and vapidKey are available) based on the notification setting toggle state.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • matheusfillipe

Poem

🐰 A fuzzy rabbit bounces with glee,
"Web push brings DM alerts to thee!
Parse the IRC, manifest the name,
Notify your users—let notifications flame!" 🔔

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: soju.im/webpush push notifications' directly and clearly summarizes the main change: adding support for the soju.im/webpush IRCv3 extension to enable push notifications.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/webpush

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (22)
src/locales/cs/messages.po-877-880 (1)

877-880: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing Czech translations for newly added notification strings.

Line 877 and Line 2244 have empty msgstr, so Czech users will see English fallback text for these new settings.

Also applies to: 2244-2247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/cs/messages.po` around lines 877 - 880, The Czech translations
are missing for the new notification strings—locate the msgid entries (e.g.,
"Enable Notifications" and the other notification msgid(s) referenced in the
diff) in src/locales/cs/messages.po and fill their corresponding msgstr fields
with proper Czech translations (ensure both occurrences noted in the review are
updated so users don't see English fallbacks); verify translation accuracy and
save the .po file so the build picks up the new Czech strings.
src/locales/de/messages.po-877-880 (1)

877-880: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

German translations are missing for new web push settings.

Line 877 and Line 2244 are untranslated (msgstr ""), so the new notification UI will render in English for de.

Also applies to: 2244-2247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/de/messages.po` around lines 877 - 880, Add German translations
for the new web push settings in the de messages.po file by replacing the empty
msgstr values for the untranslated msgid entries (including msgid "Enable
Notifications" and the other new web-push-related msgid's in that section) with
appropriate German text (e.g., "Benachrichtigungen aktivieren" for "Enable
Notifications"), ensure the translations are UTF-8 and msgfmt-compatible, and
save the updated src/locales/de/messages.po so the UI renders in German.
src/locales/es/messages.mjs-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Spanish catalog still contains English text for the new notification entries.

Line 1 includes untranslated values for the new web push strings, so es users will see English in this settings section.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/es/messages.mjs` at line 1, The messages export contains English
text for new web-push/notification entries (notably the key "DZ6N9L" and the
app-slogan key "ObsidianIRC - Bringing IRC to the future") so Spanish users see
English; open the messages JSON (export const messages) and replace those
English values with the correct Spanish translations (e.g., translate the
"DZ6N9L" web push description and the slogan value) and save the updated JSON
string so the messages constant returns fully localized Spanish text.
src/locales/es/messages.po-877-880 (1)

877-880: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fill missing Spanish translations for new notification strings.

Both new entries have empty msgstr, so Spanish users will see English text in Settings.

🌐 Proposed fix
 #: src/lib/settings/definitions/allSettings.ts
 msgid "Enable Notifications"
-msgstr ""
+msgstr "Activar notificaciones"

 #: src/lib/settings/definitions/allSettings.ts
 msgid "Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed."
-msgstr ""
+msgstr "Mostrar notificaciones de escritorio para menciones y mensajes directos. En servidores que admiten push (soju.im/webpush), esto también activa este dispositivo cuando la aplicación está cerrada."

Also applies to: 2244-2247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/es/messages.po` around lines 877 - 880, Add Spanish translations
for the new notification-related msgid entries so users see localized text:
update the msgstr for msgid "Enable Notifications" (and the other empty entries
around lines referenced, e.g., the entries at 2244-2247) with appropriate
Spanish translations (for example "Activar notificaciones" or context-accurate
variants) in src/locales/es/messages.po so both occurrences are no longer empty.
src/locales/fi/messages.po-877-880 (1)

877-880: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add missing Finnish translations for the new web push settings text.

Both msgstr values are empty, so the UI falls back to English for Finnish users.

🌐 Proposed fix
 #: src/lib/settings/definitions/allSettings.ts
 msgid "Enable Notifications"
-msgstr ""
+msgstr "Ota ilmoitukset käyttöön"

 #: src/lib/settings/definitions/allSettings.ts
 msgid "Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed."
-msgstr ""
+msgstr "Näytä työpöytäilmoitukset maininnoista ja yksityisviesteistä. Palvelimilla, jotka tukevat push-ilmoituksia (soju.im/webpush), tämä myös herättää laitteen, kun sovellus on suljettu."

Also applies to: 2244-2247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/fi/messages.po` around lines 877 - 880, Populate the empty
Finnish translation entries for the new web push settings by replacing the blank
msgstr values for the msgid "Enable Notifications" (and the related web push
settings msgid pair mentioned) with the correct Finnish strings; locate the
msgid "Enable Notifications" and the other empty msgid occurrences in
src/locales/fi/messages.po and add the appropriate Finnish translations (ensure
plural/forms if any match the original msgid usage) so the UI no longer falls
back to English.
src/locales/fr/messages.po-2244-2246 (1)

2244-2246: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing French translation for notification setting description.

The msgstr field is empty for the notification behavior description. This help text explains when notifications will wake the device and should be localized for French users.

Consider adding the French translation explaining the notification behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/fr/messages.po` around lines 2244 - 2246, Add a French
translation for the notification description msgid used in
src/lib/settings/definitions/allSettings.ts by populating the empty msgstr with
an accurate localized sentence; the msgid is "Show desktop notifications for
mentions and DMs. On servers that support push (soju.im/webpush) this also wakes
this device when the app is closed." — update the msgstr to a clear French
equivalent that explains desktop notifications for mentions/DMs and that on
push-enabled servers (soju.im/webpush) they will wake the device when the app is
closed.
src/locales/it/messages.mjs-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

English fallback text in Italian locale for "Enable Notifications".

The compiled Italian message bundle contains "dL7xtl":["Enable Notifications"] which is English text rather than an Italian translation. This suggests the source Italian .po file has an empty or missing msgstr for this entry.

The notification setting should display in Italian for Italian users, e.g., "Abilita notifiche" or equivalent. Please verify and update the Italian .po source file to include the proper translation before this .mjs file is regenerated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/it/messages.mjs` at line 1, The Italian bundle contains an
English fallback for the key "dL7xtl" in src/locales/it/messages.mjs; open the
Italian .po source entry for that message id and add the proper Italian
translation (e.g., "Abilita notifiche") into the msgstr for the corresponding
msgid, then re-run the translation compile/regeneration step to produce an
updated messages.mjs so "dL7xtl" maps to the Italian text instead of "Enable
Notifications".
src/locales/fr/messages.po-877-879 (1)

877-879: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing French translation for "Enable Notifications" setting.

The msgstr field is empty, which will cause this user-facing notification setting to display in English for French users. The PR description claims to add localized strings across 18 locales including French, but this translation is incomplete.

Consider adding the French translation, e.g., msgstr "Activer les notifications".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/fr/messages.po` around lines 877 - 879, The French .po entry for
the setting key "Enable Notifications" has an empty msgstr; update the msgstr
for the msgid "Enable Notifications" in src/locales/fr/messages.po to the proper
French translation (for example, "Activer les notifications") so the UI shows
the localized string for French users.
src/locales/it/messages.po-877-880 (1)

877-880: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fill missing Italian translations for new notification strings.

Line 879 and Line 2246 have empty msgstr, so Italian users will see fallback English for these new settings labels/descriptions.

Also applies to: 2244-2247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/it/messages.po` around lines 877 - 880, Open
src/locales/it/messages.po and provide Italian translations for the missing
msgstr entries for the new notification strings—specifically set an appropriate
Italian msgstr for msgid "Enable Notifications" and for the other empty msgstr
block around lines 2244-2247 (the new notification label/description pair);
ensure the translations are accurate, preserve punctuation and placeholders if
any, and save the file so those msgid keys no longer fall back to English.
src/locales/ja/messages.mjs-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

New Japanese catalog entries are still in English.

The generated messages payload includes English text for the new notification label/description (dL7xtl, DZ6N9L), so JA users won’t get localized copy for this feature.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/ja/messages.mjs` at line 1, Update the Japanese locale JSON so
the new keys representing the notifications are translated: replace the English
strings for the identifiers "dL7xtl" (currently "Enable Notifications") and
"DZ6N9L" (currently the long English description about desktop notifications and
wake-on-push) with their proper Japanese translations inside the exported
messages JSON string in messages (export const messages=JSON.parse(...)); ensure
you update only those two entries' values (preserving any interpolation tokens)
so the rest of the JSON stays unchanged.
src/locales/ko/messages.mjs-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Korean compiled catalog contains untranslated English for new settings text.

The new notification entries (dL7xtl, DZ6N9L) are still English in the KO catalog, so this feature is not fully localized for Korean users.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/ko/messages.mjs` at line 1, The KO catalog still contains English
strings for keys "dL7xtl" and "DZ6N9L"; locate the messages export (messages
JSON string) and replace the English text for those keys with proper Korean
translations following the existing message array format (keep surrounding array
wrappers and any placeholders), i.e. update the values for "dL7xtl" and "DZ6N9L"
to their Korean equivalents so the compiled catalog is fully localized.
src/locales/ja/messages.po-877-880 (1)

877-880: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Populate Japanese translations for the new push-notification strings.

Line 879 and Line 2246 are empty msgstr entries, which causes English fallback for these new settings in the JA locale.

Also applies to: 2244-2247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/ja/messages.po` around lines 877 - 880, Populate the empty
Japanese translations in src/locales/ja/messages.po for the new
push-notification strings so they don't fall back to English; specifically
provide a proper msgstr for msgid "Enable Notifications" (and the other
push-notification-related msgid entries around the second gap at lines
~2244-2247) by replacing the empty msgstr values with the correct Japanese
translations.
src/locales/ko/messages.po-877-880 (1)

877-880: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fill missing Korean translations before merge.

Both new entries have empty translations (msgstr ""), so Korean users will see English fallback text in these notification settings (Line 877 and Line 2244).

Also applies to: 2244-2247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/ko/messages.po` around lines 877 - 880, Two msgstr entries in
src/locales/ko/messages.po are empty (one for msgid "Enable Notifications" and
another around the block at lines ~2244-2247); fill both msgstr values with the
correct Korean translations so users don't see English fallbacks—update the
msgstr for the "Enable Notifications" msgid and locate the other empty msgstr
near line 2244 in the same file and provide its Korean translation as well.
src/locales/pl/messages.mjs-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

New Polish notification strings are still in English.

The catalog payload still contains English values for the new notification entries (e.g., “Enable Notifications” and the desktop notifications description), so these UI strings won’t be localized for pl.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/pl/messages.mjs` at line 1, The Polish messages catalog contains
untranslated English strings—update the entries for the notification keys (e.g.,
the message id "dL7xtl" currently "Enable Notifications" and the longer
desktop-notification description under "DZ6N9L") to their proper Polish
translations; locate these keys inside the exported messages JSON in
src/locales/pl/messages.mjs and replace the English text values with the correct
Polish strings (ensuring you preserve any interpolation placeholders like
[\"mentions and DMs\", [\"soju.im/webpush\"]] or similar structure).
src/locales/nl/messages.po-877-880 (1)

877-880: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Dutch translations are missing for new notification strings.

msgstr is empty for both new strings (Line 877 and Line 2244), which will leave these labels/descriptions untranslated in the Dutch locale.

Also applies to: 2244-2247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/nl/messages.po` around lines 877 - 880, The Dutch locale is
missing translations for the new settings keys from
src/lib/settings/definitions/allSettings.ts — fill the empty msgstr entries for
the msgid "Enable Notifications" (and the other new msgid at the second location
referenced around lines 2244-2247) with correct Dutch translations (e.g.,
"Meldingen inschakelen" or the appropriate phrasing), preserving the existing PO
file formatting, escaping, and context so the labels/descriptions render in the
nl locale.
src/locales/pl/messages.po-877-880 (1)

877-880: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fill missing Polish translations for new notification strings.

msgstr is empty in both entries (Line 879 and Line 2246), so these labels/descriptions will appear untranslated for Polish users.

Also applies to: 2244-2247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/pl/messages.po` around lines 877 - 880, The Polish .po entries
for the new notification labels are missing translations—locate the msgid
"Enable Notifications" (and the other new notification-related msgid entries in
the same .po file) and fill each corresponding msgstr with the correct Polish
translation(s); ensure both occurrences of "Enable Notifications" and the
related notification strings have appropriate Polish text in their msgstr
fields, preserving existing formatting and any surrounding comments or context
markers.
src/locales/ro/messages.mjs-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Romanian bundle still contains English copy for new notification messages.

The new notification entries in this payload are not localized (e.g., "Enable Notifications" and the new desktop/webpush description), causing mixed-language UI in ro.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/ro/messages.mjs` at line 1, The Romanian messages bundle contains
untranslated English strings for the new notification entries; update the JSON
payload exported in the messages constant by replacing the English values for
the notification keys (e.g., the "Enable Notifications" entry at key "dL7xtl"
and the desktop/webpush description at key "DZ6N9L") with proper Romanian
translations, preserving any placeholders/URLs/formatting exactly as in the
original (for example keep "(soju.im/webpush)" and any HTML-like tags or
placeholders intact) so the JSON.parse string remains valid and nothing else is
modified.
src/locales/pt/messages.po-877-880 (1)

877-880: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add Portuguese translations for the newly added notification entries.

Both new entries have empty msgstr (Line 879 and Line 2246), which leaves the PT locale with untranslated UI copy.

Also applies to: 2244-2247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/pt/messages.po` around lines 877 - 880, Add Portuguese
translations for the untranslated msgid entries by filling the empty msgstr for
"Enable Notifications" and the other new notification msgid present in the PT
messages.po file; locate the entries by searching for the exact msgid strings
(e.g., "Enable Notifications" and the second new notification msgid) and provide
appropriate Portuguese translations in their corresponding msgstr fields,
ensuring proper punctuation and capitalization to match the existing locale
style.
src/locales/ro/messages.po-877-880 (1)

877-880: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Populate Romanian translations for new notification strings.

Both new entries are committed with empty msgstr, so Romanian users will see source English for these settings.

Also applies to: 2244-2247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/ro/messages.po` around lines 877 - 880, Populate the missing
Romanian translations by replacing the empty msgstr values for the new
notification strings (e.g., msgid "Enable Notifications") with their proper
Romanian translations; update the same for the other empty entries referenced
(also at the second group of entries) so that users see Romanian text instead of
English in settings like Enable Notifications.
src/locales/ru/messages.po-877-880 (1)

877-880: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add Russian translations for the two newly introduced notification messages.

These entries are currently empty, which leaves English text in the Russian locale for the new notification toggle/description.

Also applies to: 2244-2247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/ru/messages.po` around lines 877 - 880, Fill in Russian
translations for the empty PO entries: set msgstr for msgid "Enable
Notifications" and for the companion notification description entry in the same
locales/ru/messages.po file (the other newly added notification msgid elsewhere
in the file); ensure the translations are proper Russian strings replacing the
empty msgstr values so the Russian locale no longer falls back to English.
src/locales/tr/messages.po-877-879 (1)

877-879: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add Turkish translations for new web-push notification labels.

These msgstr values are empty, so users on Turkish locale will get English fallback text in Settings.

🌐 Suggested fix
 #: src/lib/settings/definitions/allSettings.ts
 msgid "Enable Notifications"
-msgstr ""
+msgstr "Bildirimleri Etkinleştir"

 #: src/lib/settings/definitions/allSettings.ts
 msgid "Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed."
-msgstr ""
+msgstr "Bahsetmeler ve DM'ler için masaüstü bildirimlerini göster. Push destekleyen sunucularda (soju.im/webpush), uygulama kapalıyken bu cihazı da uyandırır."

Also applies to: 2244-2246

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/tr/messages.po` around lines 877 - 879, The msgstr entries for
the new web-push notification labels are empty (e.g., the msgid "Enable
Notifications" and the other two notification msgid strings referenced later),
so Turkish users see English fallbacks; locate the msgid "Enable Notifications"
(and the two related notification msgid entries around the other reported lines)
in the .po file and provide correct Turkish translations by filling each
corresponding msgstr with the appropriate Turkish text, ensuring you keep msgid
unchanged and preserve PO file quoting/escaping conventions.
src/locales/sv/messages.po-877-879 (1)

877-879: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fill missing Swedish translations for new notification strings.

Both new entries have empty msgstr, so Swedish users will see English fallback text in the Notifications settings UI.

🌐 Suggested fix
 #: src/lib/settings/definitions/allSettings.ts
 msgid "Enable Notifications"
-msgstr ""
+msgstr "Aktivera aviseringar"

 #: src/lib/settings/definitions/allSettings.ts
 msgid "Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed."
-msgstr ""
+msgstr "Visa skrivbordsaviseringar för omnämnanden och privata meddelanden. På servrar som stöder push (soju.im/webpush) väcks också den här enheten när appen är stängd."

Also applies to: 2244-2246

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/sv/messages.po` around lines 877 - 879, Fill the missing Swedish
translations in src/locales/sv/messages.po by providing appropriate Swedish text
for the empty msgstr entries for the notification-related msgids (e.g., msgid
"Enable Notifications" and the other notification msgid occurrences noted in the
review); update each msgstr to the correct Swedish translation and keep the
surrounding PO entry format intact so the Notifications settings UI shows
Swedish text instead of the English fallback.
🧹 Nitpick comments (2)
src/components/ui/UserSettings.tsx (1)

700-703: ⚡ Quick win

Convert this to a single-line rationale comment.

Keep this as one line to match the repo’s TS/TSX comment style rule.

As per coding guidelines, src/**/*.{ts,tsx}: "Comments must explain why, never what ... Keep comments to one line and write in project context, not change context."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/UserSettings.tsx` around lines 700 - 703, Replace the
multi-line rationale comment above the notifications toggle in the UserSettings
component with a single-line comment that explains why the toggle exists (e.g.,
that it manages soju.im/webpush subscriptions and prompts for browser
permission) and keep the content in project context; locate the comment near the
notifications toggle logic in src/components/ui/UserSettings.tsx (the block
referencing "notifications toggle" and "soju.im/webpush") and condense it to one
line to match the repo's TS/TSX comment style.
src/lib/irc/IRCClient.ts (1)

601-603: ⚡ Quick win

Keep the capability rationale as a single-line comment.

This comment block should be collapsed to one line to match the TS/TSX comment convention used in this repo.

♻️ Proposed edit
-    // soju.im/webpush: lets us register a browser PushManager
-    // subscription with the server so DMs/highlights wake the device
-    // via the platform push service even when the tab is closed.
+    // Request soju.im/webpush so servers can wake this client for DMs/highlights while the tab is closed.
As per coding guidelines, `src/**/*.{ts,tsx}`: "Comments must explain why, never what ... Keep comments to one line and write in project context, not change context."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/irc/IRCClient.ts` around lines 601 - 603, Collapse the three-line
comment about soju.im/webpush into a single-line comment that explains why the
capability exists (e.g., "soju.im/webpush: register browser PushManager so
DMs/highlights wake device via platform push service when tab is closed") and
replace the multi-line block near the PushManager registration area in
IRCClient.ts (around the soju.im/webpush comment) with that single-line comment
to conform to the project's one-line TS/TSX comment guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/ui/UserSettings.tsx`:
- Around line 706-720: The current code fires registerWebPush/unregisterWebPush
without awaiting and runs them for all servers (which may be disconnected);
change the logic to only operate on connected servers and await the operations
(either sequentially with await inside the for..of or gather promises and await
Promise.all). Specifically, in the wantNotifications branch, after permission
=== "granted", filter servers by a connection flag (e.g., srv.connected or
srv.isConnected) and by srv.vapidKey and capabilities before calling
registerWebPush(srv.id, srv.vapidKey), and await those calls; similarly, when
wantNotifications === false, filter by the same connection flag and capabilities
then await unregisterWebPush(srv.id). Ensure you handle errors from each awaited
call (try/catch or Promise.allSettled) so failures don’t fail silently.

In `@src/locales/en/messages.po`:
- Around line 876-878: The i18n pipeline failed due to untranslated strings
(specifically the msgid "Enable Notifications"); run the extraction and
compilation scripts (npm run i18n:extract && npm run i18n:compile) locally to
regenerate the locale files, verify the msgid "Enable Notifications" is present
and translated in the generated locale files, and commit the updated files so
the i18n catalog check passes.

---

Minor comments:
In `@src/locales/cs/messages.po`:
- Around line 877-880: The Czech translations are missing for the new
notification strings—locate the msgid entries (e.g., "Enable Notifications" and
the other notification msgid(s) referenced in the diff) in
src/locales/cs/messages.po and fill their corresponding msgstr fields with
proper Czech translations (ensure both occurrences noted in the review are
updated so users don't see English fallbacks); verify translation accuracy and
save the .po file so the build picks up the new Czech strings.

In `@src/locales/de/messages.po`:
- Around line 877-880: Add German translations for the new web push settings in
the de messages.po file by replacing the empty msgstr values for the
untranslated msgid entries (including msgid "Enable Notifications" and the other
new web-push-related msgid's in that section) with appropriate German text
(e.g., "Benachrichtigungen aktivieren" for "Enable Notifications"), ensure the
translations are UTF-8 and msgfmt-compatible, and save the updated
src/locales/de/messages.po so the UI renders in German.

In `@src/locales/es/messages.mjs`:
- Line 1: The messages export contains English text for new
web-push/notification entries (notably the key "DZ6N9L" and the app-slogan key
"ObsidianIRC - Bringing IRC to the future") so Spanish users see English; open
the messages JSON (export const messages) and replace those English values with
the correct Spanish translations (e.g., translate the "DZ6N9L" web push
description and the slogan value) and save the updated JSON string so the
messages constant returns fully localized Spanish text.

In `@src/locales/es/messages.po`:
- Around line 877-880: Add Spanish translations for the new notification-related
msgid entries so users see localized text: update the msgstr for msgid "Enable
Notifications" (and the other empty entries around lines referenced, e.g., the
entries at 2244-2247) with appropriate Spanish translations (for example
"Activar notificaciones" or context-accurate variants) in
src/locales/es/messages.po so both occurrences are no longer empty.

In `@src/locales/fi/messages.po`:
- Around line 877-880: Populate the empty Finnish translation entries for the
new web push settings by replacing the blank msgstr values for the msgid "Enable
Notifications" (and the related web push settings msgid pair mentioned) with the
correct Finnish strings; locate the msgid "Enable Notifications" and the other
empty msgid occurrences in src/locales/fi/messages.po and add the appropriate
Finnish translations (ensure plural/forms if any match the original msgid usage)
so the UI no longer falls back to English.

In `@src/locales/fr/messages.po`:
- Around line 2244-2246: Add a French translation for the notification
description msgid used in src/lib/settings/definitions/allSettings.ts by
populating the empty msgstr with an accurate localized sentence; the msgid is
"Show desktop notifications for mentions and DMs. On servers that support push
(soju.im/webpush) this also wakes this device when the app is closed." — update
the msgstr to a clear French equivalent that explains desktop notifications for
mentions/DMs and that on push-enabled servers (soju.im/webpush) they will wake
the device when the app is closed.
- Around line 877-879: The French .po entry for the setting key "Enable
Notifications" has an empty msgstr; update the msgstr for the msgid "Enable
Notifications" in src/locales/fr/messages.po to the proper French translation
(for example, "Activer les notifications") so the UI shows the localized string
for French users.

In `@src/locales/it/messages.mjs`:
- Line 1: The Italian bundle contains an English fallback for the key "dL7xtl"
in src/locales/it/messages.mjs; open the Italian .po source entry for that
message id and add the proper Italian translation (e.g., "Abilita notifiche")
into the msgstr for the corresponding msgid, then re-run the translation
compile/regeneration step to produce an updated messages.mjs so "dL7xtl" maps to
the Italian text instead of "Enable Notifications".

In `@src/locales/it/messages.po`:
- Around line 877-880: Open src/locales/it/messages.po and provide Italian
translations for the missing msgstr entries for the new notification
strings—specifically set an appropriate Italian msgstr for msgid "Enable
Notifications" and for the other empty msgstr block around lines 2244-2247 (the
new notification label/description pair); ensure the translations are accurate,
preserve punctuation and placeholders if any, and save the file so those msgid
keys no longer fall back to English.

In `@src/locales/ja/messages.mjs`:
- Line 1: Update the Japanese locale JSON so the new keys representing the
notifications are translated: replace the English strings for the identifiers
"dL7xtl" (currently "Enable Notifications") and "DZ6N9L" (currently the long
English description about desktop notifications and wake-on-push) with their
proper Japanese translations inside the exported messages JSON string in
messages (export const messages=JSON.parse(...)); ensure you update only those
two entries' values (preserving any interpolation tokens) so the rest of the
JSON stays unchanged.

In `@src/locales/ja/messages.po`:
- Around line 877-880: Populate the empty Japanese translations in
src/locales/ja/messages.po for the new push-notification strings so they don't
fall back to English; specifically provide a proper msgstr for msgid "Enable
Notifications" (and the other push-notification-related msgid entries around the
second gap at lines ~2244-2247) by replacing the empty msgstr values with the
correct Japanese translations.

In `@src/locales/ko/messages.mjs`:
- Line 1: The KO catalog still contains English strings for keys "dL7xtl" and
"DZ6N9L"; locate the messages export (messages JSON string) and replace the
English text for those keys with proper Korean translations following the
existing message array format (keep surrounding array wrappers and any
placeholders), i.e. update the values for "dL7xtl" and "DZ6N9L" to their Korean
equivalents so the compiled catalog is fully localized.

In `@src/locales/ko/messages.po`:
- Around line 877-880: Two msgstr entries in src/locales/ko/messages.po are
empty (one for msgid "Enable Notifications" and another around the block at
lines ~2244-2247); fill both msgstr values with the correct Korean translations
so users don't see English fallbacks—update the msgstr for the "Enable
Notifications" msgid and locate the other empty msgstr near line 2244 in the
same file and provide its Korean translation as well.

In `@src/locales/nl/messages.po`:
- Around line 877-880: The Dutch locale is missing translations for the new
settings keys from src/lib/settings/definitions/allSettings.ts — fill the empty
msgstr entries for the msgid "Enable Notifications" (and the other new msgid at
the second location referenced around lines 2244-2247) with correct Dutch
translations (e.g., "Meldingen inschakelen" or the appropriate phrasing),
preserving the existing PO file formatting, escaping, and context so the
labels/descriptions render in the nl locale.

In `@src/locales/pl/messages.mjs`:
- Line 1: The Polish messages catalog contains untranslated English
strings—update the entries for the notification keys (e.g., the message id
"dL7xtl" currently "Enable Notifications" and the longer desktop-notification
description under "DZ6N9L") to their proper Polish translations; locate these
keys inside the exported messages JSON in src/locales/pl/messages.mjs and
replace the English text values with the correct Polish strings (ensuring you
preserve any interpolation placeholders like [\"mentions and DMs\",
[\"soju.im/webpush\"]] or similar structure).

In `@src/locales/pl/messages.po`:
- Around line 877-880: The Polish .po entries for the new notification labels
are missing translations—locate the msgid "Enable Notifications" (and the other
new notification-related msgid entries in the same .po file) and fill each
corresponding msgstr with the correct Polish translation(s); ensure both
occurrences of "Enable Notifications" and the related notification strings have
appropriate Polish text in their msgstr fields, preserving existing formatting
and any surrounding comments or context markers.

In `@src/locales/pt/messages.po`:
- Around line 877-880: Add Portuguese translations for the untranslated msgid
entries by filling the empty msgstr for "Enable Notifications" and the other new
notification msgid present in the PT messages.po file; locate the entries by
searching for the exact msgid strings (e.g., "Enable Notifications" and the
second new notification msgid) and provide appropriate Portuguese translations
in their corresponding msgstr fields, ensuring proper punctuation and
capitalization to match the existing locale style.

In `@src/locales/ro/messages.mjs`:
- Line 1: The Romanian messages bundle contains untranslated English strings for
the new notification entries; update the JSON payload exported in the messages
constant by replacing the English values for the notification keys (e.g., the
"Enable Notifications" entry at key "dL7xtl" and the desktop/webpush description
at key "DZ6N9L") with proper Romanian translations, preserving any
placeholders/URLs/formatting exactly as in the original (for example keep
"(soju.im/webpush)" and any HTML-like tags or placeholders intact) so the
JSON.parse string remains valid and nothing else is modified.

In `@src/locales/ro/messages.po`:
- Around line 877-880: Populate the missing Romanian translations by replacing
the empty msgstr values for the new notification strings (e.g., msgid "Enable
Notifications") with their proper Romanian translations; update the same for the
other empty entries referenced (also at the second group of entries) so that
users see Romanian text instead of English in settings like Enable
Notifications.

In `@src/locales/ru/messages.po`:
- Around line 877-880: Fill in Russian translations for the empty PO entries:
set msgstr for msgid "Enable Notifications" and for the companion notification
description entry in the same locales/ru/messages.po file (the other newly added
notification msgid elsewhere in the file); ensure the translations are proper
Russian strings replacing the empty msgstr values so the Russian locale no
longer falls back to English.

In `@src/locales/sv/messages.po`:
- Around line 877-879: Fill the missing Swedish translations in
src/locales/sv/messages.po by providing appropriate Swedish text for the empty
msgstr entries for the notification-related msgids (e.g., msgid "Enable
Notifications" and the other notification msgid occurrences noted in the
review); update each msgstr to the correct Swedish translation and keep the
surrounding PO entry format intact so the Notifications settings UI shows
Swedish text instead of the English fallback.

In `@src/locales/tr/messages.po`:
- Around line 877-879: The msgstr entries for the new web-push notification
labels are empty (e.g., the msgid "Enable Notifications" and the other two
notification msgid strings referenced later), so Turkish users see English
fallbacks; locate the msgid "Enable Notifications" (and the two related
notification msgid entries around the other reported lines) in the .po file and
provide correct Turkish translations by filling each corresponding msgstr with
the appropriate Turkish text, ensuring you keep msgid unchanged and preserve PO
file quoting/escaping conventions.

---

Nitpick comments:
In `@src/components/ui/UserSettings.tsx`:
- Around line 700-703: Replace the multi-line rationale comment above the
notifications toggle in the UserSettings component with a single-line comment
that explains why the toggle exists (e.g., that it manages soju.im/webpush
subscriptions and prompts for browser permission) and keep the content in
project context; locate the comment near the notifications toggle logic in
src/components/ui/UserSettings.tsx (the block referencing "notifications toggle"
and "soju.im/webpush") and condense it to one line to match the repo's TS/TSX
comment style.

In `@src/lib/irc/IRCClient.ts`:
- Around line 601-603: Collapse the three-line comment about soju.im/webpush
into a single-line comment that explains why the capability exists (e.g.,
"soju.im/webpush: register browser PushManager so DMs/highlights wake device via
platform push service when tab is closed") and replace the multi-line block near
the PushManager registration area in IRCClient.ts (around the soju.im/webpush
comment) with that single-line comment to conform to the project's one-line
TS/TSX comment guideline.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 78fa61cf-c92c-4c57-9843-16eb9e55753c

📥 Commits

Reviewing files that changed from the base of the PR and between 2257f1c and 8c04c73.

📒 Files selected for processing (46)
  • public/sw.js
  • src/components/ui/UserSettings.tsx
  • src/lib/irc/IRCClient.ts
  • src/lib/settings/definitions/allSettings.ts
  • src/lib/webpush.ts
  • src/locales/cs/messages.mjs
  • src/locales/cs/messages.po
  • src/locales/de/messages.mjs
  • src/locales/de/messages.po
  • src/locales/en/messages.mjs
  • src/locales/en/messages.po
  • src/locales/es/messages.mjs
  • src/locales/es/messages.po
  • src/locales/fi/messages.mjs
  • src/locales/fi/messages.po
  • src/locales/fr/messages.mjs
  • src/locales/fr/messages.po
  • src/locales/it/messages.mjs
  • src/locales/it/messages.po
  • src/locales/ja/messages.mjs
  • src/locales/ja/messages.po
  • src/locales/ko/messages.mjs
  • src/locales/ko/messages.po
  • src/locales/nl/messages.mjs
  • src/locales/nl/messages.po
  • src/locales/pl/messages.mjs
  • src/locales/pl/messages.po
  • src/locales/pt/messages.mjs
  • src/locales/pt/messages.po
  • src/locales/ro/messages.mjs
  • src/locales/ro/messages.po
  • src/locales/ru/messages.mjs
  • src/locales/ru/messages.po
  • src/locales/sv/messages.mjs
  • src/locales/sv/messages.po
  • src/locales/tr/messages.mjs
  • src/locales/tr/messages.po
  • src/locales/uk/messages.mjs
  • src/locales/uk/messages.po
  • src/locales/zh-TW/messages.mjs
  • src/locales/zh-TW/messages.po
  • src/locales/zh/messages.mjs
  • src/locales/zh/messages.po
  • src/protocol/isupport.ts
  • src/store/handlers/connection.ts
  • src/types/index.ts

Comment on lines +706 to +720
if (wantNotifications) {
const permission = await requestNotificationPermission();
if (permission === "granted") {
for (const srv of servers) {
if (srv.vapidKey && srv.capabilities?.includes("soju.im/webpush")) {
void registerWebPush(srv.id, srv.vapidKey);
}
}
}
} else if (wantNotifications === false) {
for (const srv of servers) {
if (srv.capabilities?.includes("soju.im/webpush")) {
void unregisterWebPush(srv.id);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle push subscription calls as awaited tasks on connected servers only.

This currently dispatches registration/unregistration as fire-and-forget across all servers, which can fail silently and attempt work against disconnected servers.

🛠️ Proposed fix
+    const pushServers = servers.filter(
+      (srv) =>
+        srv.isConnected && srv.capabilities?.includes("soju.im/webpush"),
+    );
+
     if (wantNotifications) {
       const permission = await requestNotificationPermission();
       if (permission === "granted") {
-        for (const srv of servers) {
-          if (srv.vapidKey && srv.capabilities?.includes("soju.im/webpush")) {
-            void registerWebPush(srv.id, srv.vapidKey);
-          }
-        }
+        const tasks = pushServers
+          .filter((srv) => !!srv.vapidKey)
+          .map((srv) => registerWebPush(srv.id, srv.vapidKey!));
+        await Promise.allSettled(tasks);
       }
     } else if (wantNotifications === false) {
-      for (const srv of servers) {
-        if (srv.capabilities?.includes("soju.im/webpush")) {
-          void unregisterWebPush(srv.id);
-        }
-      }
+      const tasks = pushServers.map((srv) => unregisterWebPush(srv.id));
+      await Promise.allSettled(tasks);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/UserSettings.tsx` around lines 706 - 720, The current code
fires registerWebPush/unregisterWebPush without awaiting and runs them for all
servers (which may be disconnected); change the logic to only operate on
connected servers and await the operations (either sequentially with await
inside the for..of or gather promises and await Promise.all). Specifically, in
the wantNotifications branch, after permission === "granted", filter servers by
a connection flag (e.g., srv.connected or srv.isConnected) and by srv.vapidKey
and capabilities before calling registerWebPush(srv.id, srv.vapidKey), and await
those calls; similarly, when wantNotifications === false, filter by the same
connection flag and capabilities then await unregisterWebPush(srv.id). Ensure
you handle errors from each awaited call (try/catch or Promise.allSettled) so
failures don’t fail silently.

Comment on lines +876 to +878
#: src/lib/settings/definitions/allSettings.ts
msgid "Enable Notifications"
msgstr "Enable Notifications"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Pipeline failure: i18n catalog check failed.

The GitHub Actions i18n check is failing with: "Untranslated strings detected in source." This indicates the i18n extraction and compilation process may not have completed correctly.

Please run:

npm run i18n:extract && npm run i18n:compile

and commit any changes to the src/locales/ files to resolve the pipeline failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/en/messages.po` around lines 876 - 878, The i18n pipeline failed
due to untranslated strings (specifically the msgid "Enable Notifications"); run
the extraction and compilation scripts (npm run i18n:extract && npm run
i18n:compile) locally to regenerate the locale files, verify the msgid "Enable
Notifications" is present and translated in the generated locale files, and
commit the updated files so the i18n catalog check passes.


#: src/lib/settings/definitions/allSettings.ts
msgid "Enable Notifications"
msgstr ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are just skipping adding any translation everywhere though? 🤔 I only noticed now @ValwareIRC

@ValwareIRC

ValwareIRC commented Jun 2, 2026 via email

Copy link
Copy Markdown
Contributor Author

ValwareIRC added a commit that referenced this pull request Jun 5, 2026
Matt's right that this doesn't belong here. The whole
src-tauri/plugins/unifiedpush/ tree got picked up by accident in
fc50a0f ('desktop: surface connect failures...'); my working tree had
the parallel feature/webpush branch's scaffolding sitting in the same
src-tauri/plugins/ directory and `git add -A` swept it in.

Nothing on this branch actually wires the plugin in -- no references
in src-tauri/src/, Cargo.toml, tauri.conf.json, or anywhere in src/.
It's orphan scaffolding. The plugin lives on feature/webpush (PR #236)
where it's actually built into the app.

Removed all 30 remaining files (the previous chore commit only got
android/build/ + Cargo.lock, 148 files; this clears the rest -- the
android/.tauri/ tauri-api stubs and the empty plugin shell).

Build + 845 tests still green; nothing on this branch was depending on it.
@bb010g

bb010g commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Support on Android for Web Push via UnifiedPush would be nice, but no pressure to include it in this initial PR.

@ValwareIRC

Copy link
Copy Markdown
Contributor Author

Support on Android for Web Push via UnifiedPush would be nice, but no pressure to include it in this initial PR.

It's coming :)

@bb010g

bb010g commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

I should also explicitly note UnifiedPush on Linux, which you could bind to from Tauri using the unifiedpush crate.

ValwareIRC added a commit that referenced this pull request Jun 14, 2026
* feat(ai-tools): draft/ai-tools workflow viewer

Implements client-side support for the draft/ai-tools v0.4 spec:
AI bots stream their workflow state (thinking, tool calls, results)
as TAGMSG carrying a +obby.world/ai-tools JSON envelope; we render
those workflows in a floating tray pinned to the top-right of the
chat area, scoped to the current channel.

Each workflow is its own card -- spinner + bot nick + dropdown
chevron when collapsed, expands to show a Claude-Code-style
timeline of steps (colored dot accent per type, content rendered
in a monospace box for both string fragments and tool-call args).
Multiple bots → multiple stacked cards, so workflow telemetry
never eats into chat real-estate.

Control signals (cancel / approve / reject) round-trip via TAGMSG
to the bot's nick. Pending-approval steps surface inline buttons
inside the card; the card header has a Stop button while running
and a dismiss-X after the workflow reaches a terminal state.

Includes:
- aiTools.ts: decoder/encoder + TS types matching the spec
- src/store/handlers/aiTools.ts: TAGMSG/CHANMSG/USERMSG → workflow
  state machine; string-content fragments concatenate so the
  ai-tools/content-stream batch reassembles correctly even when
  processed message-by-message
- AiToolsCard + AiToolsTray: the floating UI
- ChatArea wires the tray in, scoped to the selected target
- draft/ai-tools added to the CAP REQ list
- Tests: 21 new (decoder edge cases + handler state transitions)
- Translations: all 18 supported locales

* feat(ai-tools): recursive badge renderer for structured step content

Replaces the flat JSON.stringify <pre> dump with a key→value chip
tree. Primitives become inline colored chips (green strings, cyan
numbers, purple bools); nested objects/arrays nest under a tinted
left rule with their entries laid out one-per-row. Much easier to
scan tool-call args once they get more than a couple of fields,
which the user noticed when the bot started passing structured
options through.

* feat(ai-tools): two-way deep link between card and final PRIVMSG

* feat(ai-tools): workflow history button in chat header

* i18n(ai-tools): translate workflow history button + step count

'Workflow history' (button title) and '{0} step(s)' (list row summary)
across all 18 non-English locales.

* feat(ai-tools): consistent project-diagram icon, inline pill

* fix(ai-tools): float pill left so it sits inline with first line of body

CollapsibleMessage renders as a block-level div, so the inline-flex
pill stacked above the message body rather than sitting next to it.
Float-left makes the body wrap around the chip, prepending it to the
first line as intended.

* feat(ai-tools): card auto-dismiss countdown, header offset, auto-scroll

* fix(ai-tools): pill clickable + vertical connector between step dots

* feat(ai-tools): show prompt under bot nick on workflow card

Extends the workflow message schema with an optional `prompt` field
that bots can use to ship a short truncated copy of the trigger
message. The card renders it in italic muted text under the workflow
name so users see what was asked without scrolling back to the
trigger PRIVMSG. Field is optional -- bots that don't include it are
unaffected, the card just omits the line.

Plumbed end-to-end: lib/aiTools.ts decoder, AiWorkflow store type,
applyWorkflowUpdate merge, AiToolsCard render.

* feat(ai-tools): drop card auto-dismiss to 5s, fade across full window

Going from 60s to 5s means the previous 'fade only in the last 10s'
logic would start with the card already half-transparent, so switch
the opacity easing to span the full countdown (1.0 -> 0.15) instead.

* feat(ai-tools): show count badge next to workflow history icon

* i18n(ai-tools): translate 'Workflow history ({0})' tooltip

* feat(ai-tools): mute the inline workflow pill

* feat(ai-tools): redesign workflow surface as in-chat placeholder

Live workflows now claim a chat slot the moment the start TAGMSG lands.
The slot shows the workflow name + a live preview of the latest step
with a spinner, then morphs in place into the bot's final PRIVMSG
(carrying the workflow pill) once it arrives -- no row jump.

Historical (chathistory-replayed) workflows no longer pop floating
tray cards on channel join; they remain in the workflow history popover
and the inline pill on the original message.

* fix(ai-tools): preserve reply context on morph + clean up stuck placeholder

Two issues with the in-chat placeholder flow:

1. When the bot's tagged PRIVMSG morphs the placeholder, we were not
   resolving the +reply tag, so the morphed row had no reply quote
   block (whereas an untagged reply landed via the normal path with
   quote intact).  Now resolveReplyMessage + extractMentions run on
   the morph path too.

2. If the bot's final PRIVMSG doesn't carry the workflow tag (only a
   terminal-state TAGMSG is sent), the morph path never fires and the
   placeholder is stuck on a "Thinking…" spinner while the bot's
   actual reply lands as a separate row.  On terminal-state TAGMSG,
   remove the still-pending placeholder so the bot's reply can land
   normally with full reply context.

* fix(ai-tools): allow clicking inline pill to view historical workflows

The tray filters out historical workflows so chathistory replay
doesn't pop a wall of cards on channel join, but the inline pill's
reopen action was leaving `historical` set -- meaning even after
explicit click the card stayed filtered out and nothing happened.
Clear it on reopen so an explicit view request always surfaces the
card.

* feat(ai-tools): hang inline pill in avatar gutter instead of inline

The pill used to sit in a flex column to the left of the body, which
pushed the entire message content sideways and made the reply look
visually off-axis from neighbouring messages.  Strip it to a bare
clickable icon (step count + name already live on the floating card
and history popover) and absolutely-position it in the avatar gutter
so the body keeps its natural alignment.

* feat(ai-tools): pair tool-call+result as one IN/OUT row, drop thinking from count

Two changes to how workflow steps are presented:

1. Display: a tool-call and its matching tool-result now render as a
   single row -- tool name in the header, then IN (call args) and OUT
   (result content) stacked, matching the Claude Code "Bash → Commit
   and push" pattern.  Pairing is FIFO by tool name; an orphan result
   (no preceding call) still renders.  Thinking / text rows are
   unchanged.

2. Counting: countableSteps() now powers the inline pill tooltip and
   the history popover label.  It excludes thinking (model muttering,
   not work) and counts a tool-call/result pair as one step instead
   of two.

* feat(ai-tools): double the workflow card width

* fix(ai-tools): scroll workflow card body to bottom on open

The auto-scroll effect only fired when the user was already at the
bottom, which never happens on initial expand (scrollTop is 0).
Track a "has-done-initial-scroll" flag per expansion: force-scroll
to the bottom the first time the body mounts/expands, then fall back
to the at-bottom check for subsequent step updates so a user
scrolled-up to review earlier output isn't yanked away.

* fix(ai-tools): track scroll stickiness via onScroll, not post-update check

The previous approach checked "is the user at the bottom?" inside
the content-update effect, but by then the new content had already
grown scrollHeight -- the old scrollTop no longer qualified as
bottom, so we never auto-scrolled even when the user was parked
there.

Track stickiness via the scroll event instead: whenever the user
(or we) scrolls, recompute whether they're at the bottom and stash
that into a ref.  On content update, honour the flag.  Reset to
sticky on collapse so a fresh expansion always lands on the latest
content.

* client: implement draft/bot-cmds discovery + +draft/bot-cmd invocation

Adds first-class support for PushBot-style slash commands as defined by
the obbyircd doc/pushbot-spec.md protocol.  The user types /forecast
london in #weather and the client:

  1. Looks up "forecast" in the per-server botCommands cache (keyed
     by lowercased bot nick).
  2. If a bot in the current channel exposes that command, builds a
     base64-encoded JSON payload { name, options } and sends
     @+draft/bot-cmd=<b64> TAGMSG <#channel|botnick>, picking the
     PRIVMSG-style "public" or NOTICE-style "private" wire form based
     on the command's visibility field.
  3. Falls back to the raw IRC command path if no match -- existing
     /op, /me etc. behaviour is preserved.

Discovery is event-driven:

  * draft/bot-cmds added to ourCaps so the server knows we're aware
    of the protocol (informational; the server still does the work).
  * registerPushBotHandlers (new src/store/handlers/pushbot.ts) hooks
    TAGMSG, decodes +draft/bot-cmds responses, and writes to
    server.botCommands.  A +draft/bot-cmds-changed broadcast clears
    the cached entry so the next slash invocation triggers a refetch.
  * The handler is wired into registerAllHandlers in
    src/store/handlers/index.ts.

Types extended:
  * Server.botCommands: Record<botNick, BotCommand[]> on src/types.
  * BotCommand / BotCommandOption mirror the JSON the bot publishes
    (name, description, visibility, scopes, options[]).

Resolution order in tryDispatchBotCommand mirrors §7.5 of the spec:
explicit /cmd@botnick targets first, then channel-bots, then DM
partners, then server-wide bots.  Public invocations go to the
channel (everyone sees the reply); private ones go to the bot with
+draft/channel-context to keep whisper-style replies routed to the
right view.

Tests: tests/store/pushbot.test.ts covers cache population,
+draft/bot-cmds-changed invalidation, and that unrelated TAGMSGs are
ignored.  All 60 test files (789 tests) still pass.

* client: auto-query bot slash commands on WHO completion

After RPL_ENDOFWHO (315) for a channel, scan the channel's user list
for users marked isBot=true (set by handleWhoxReply when the +B mode
flag is present) and send a +draft/bot-cmds-query TAGMSG to each
that we don't already have cached.  Result: by the time the user
types '/' in a channel with bots in it, the autocomplete list is
already populated.

* client: surface PushBot commands in the slash autocomplete

Two visible-to-the-user gaps now closed:

1. ChatArea rendered the slash popover from `cmdsAvailable` only.
   Merge in command names from `server.botCommands` so /forecast
   shows up in the popover when the user is in a channel with a
   bot that has registered it.

2. Discovery used to fire only on WHO_END.  Add a lazy
   `queryUncachedBotsInChannel` triggered the first time the user
   starts a slash command in a channel that has +B users without
   cached schemas, so the popover catches up if WHO completed
   before the handler attached.

* client: rich slash-cmd popover + per-arg hint footer

Previously the popover rendered every suggestion as just `/name`; the
caller didn't know whether the source was a server-side built-in or a
PushBot, and there was no signal that /forecast even takes arguments.

Now:

* `SlashSuggestion` carries name + description + options[] + source
  ({kind:"builtin"} or {kind:"bot", botNick, scope:"channel"|"server"}).
  ChatArea builds these from cmdsAvailable (builtins) and from
  server.botCommands (PushBot schemas).
* The popover renders the bare name plus a "channel-bot" / "server-bot"
  badge with the bot nick, the description below, and `<required>` /
  `[optional]` placeholders inline next to the name.  Channel-bots
  only appear when their bot is a member of the active channel;
  server-wide bots show up everywhere we have a cached schema.
* A new SlashParamHint floats above the input once the user has typed
  past the command name -- it highlights the active argument (the one
  the cursor is currently in), shows the param's type, "required"
  tag, description, and any `choices` list.  Disappears for builtins
  (no schema) and once the user has scrolled past all declared opts.

Tests: 6 new cases for getActiveParamContext covering cmd-name
typing, `//foo` escape, position 0..N argument tracking, `/cmd@bot`
targeting suffix, and case-insensitive cmd matching.  All 61 test
files (795 tests) green.

* client: show client + server commands in slash popover with badges

The popover was missing the React-side commands (/me, /msg, /nick,
/whisper, /join, /part, /away, /back) because they're handled
locally before they touch the wire and never appear in the
obsidianirc/cmdslist set.  Centralized them in
src/lib/clientCommands.ts with full schemas (description + options)
so the popover and the param-hint render them identically to
PushBot commands.

Source kinds now distinguish:
  * client → handled locally; slate badge, "(handled by ObsidianIRC)"
  * server → from obsidianirc/cmdslist; emerald badge
  * bot → draft/bot-cmds; amber "channel-bot" or purple "server-bot"

Dedup is client > server > bot, so /me always renders as client
even if the server's cmdslist also advertises it.  Badges have
hover-tooltips explaining the source.

* client: obby.world/channel-bots cap + Bots management modal

Two passes squashed:

(1) Negotiate the obby.world/channel-bots capability, receive the
    server's bot directory burst at welcome time (BATCH wrapper, one
    TAGMSG per bot carrying obby.world/bot-info=<base64-json>) plus
    incremental add/update/remove pushes.  State lands in server.bots
    (Record<lowerNick, PushBotInfo>) and mirrors into botCommands so
    the slash popover stays warm without a separate
    +draft/bot-cmds-query.

(2) New BotsModal — left pane: filter (All / Server-wide / Channel) +
    search + scope/status badges and online dot; right pane: realname,
    transport, joined channels, command schemas, IRCop action buttons
    (Approve / Suspend / Unsuspend / Delete) for non-config-defined
    bots.  Wired into ChatHeader via onOpenBots, both as a desktop
    icon button (🤖, hidden md:block) and as an overflow-menu entry
    for narrow/mobile views -- the first cut only added the overflow
    entry which is invisible on desktop widths.

Tests: 2 new vitest cases for the bot-info pipeline (add populates
server.bots + botCommands; remove clears both).  61 test files,
797 tests green.

* client: rebuild BotsModal to match UserSettings layout + drop emoji icons

The first cut of BotsModal was a one-off custom layout (flex split,
non-portal, ad-hoc styling) using a 🤖 emoji as a header decoration.
That worked but it didn't match the rest of the app and looked off
on mobile.

Rework it to mirror UserSettings:
* useMediaQuery to branch desktop vs mobile
* useModalBehavior for escape / click-outside
* desktop: backdrop + centered card (max-w-4xl, h-80vh) with a fixed-
  width sidebar (filterable bot list) and a content pane (selected
  bot detail); Discord-dark palette and discord-primary accents.
* mobile: full-screen createPortal with two views (list → detail
  drill-in, back button to return), safe-area padding matching
  UserSettings.

Icons: every emoji used as UI chrome now uses react-icons/fa.
  * 🤖 channel-header button → <FaRobot />
  * 🤖 overflow-menu entry → <FaRobot />
  * online indicator dot in the bot list → <FaCircle />
  * empty-state placeholder → <FaRobot className="text-4xl" />

Same surface area: filter chips, search input, status/scope badges,
IRCop action buttons (Approve / Suspend / Unsuspend / Delete) for
non-config-defined bots.  The 🤖 next to bot nicknames in chat is
unchanged -- that's a pre-existing bot identity marker, not UI chrome.

* slash picker: show server-scope bots, prefer bot names over server cmdslist

Two bugs:

1) Server-scope bots (helpbot, dicebot) never showed up in the picker.
   The picker skipped any bot that wasn't a channel member, which was
   right for channel-scope bots but wrong for server-scope bots that
   never auto-join.  Now: gate the membership check on the bot's
   scope; server-scope bots are always offered.

2) When a server's cmdsAvailable advertised a name (e.g. HELP) that
   a bot also defined (helpbot's /help), the server entry won the
   dedup set and the bot was shadowed; the hint code meanwhile pulled
   the bot's schema for that name, producing the "picker says it's
   the built-in but the hint reads like the bot" mismatch.  Process
   bot commands before cmdsAvailable so the canonical bot owner wins
   the seen-set, and apply the same scope filter to the hint schemas.

Drive-by: replace `choices!.length` with `(choices?.length ?? 0)` in
SlashParamHint that fix:unsafe had downgraded to an unsafe optional
chain on the comparator.

* slash picker: multi-bot same-name + correct anchor + brand highlight

Three UI/UX issues addressed:

1) Two bots can now publish the same command name and both show up
   as distinct entries in the picker.  Cross-bot dedup is dropped,
   the existing `@<botNick>` badge differentiates rows visually.
   On select, the picker fills `/<cmd>@<botNick> ` so the existing
   useMessageSending `/cmd@bot` routing dispatches unambiguously.
   The bare-name reservation against server cmdsAvailable stays, so
   the built-in /HELP doesn't appear next to a bot's /help.

2) SlashCommandPopover now anchors via `bottom` (= input.top + 6px
   gap) instead of `top + estimated-height`.  The earlier estimate
   used a per-row height that under-counted multi-line entries, so
   the popover drifted away from the input start when more rows or
   longer descriptions were present.  `bottom` keeps it flush.

3) Selected-row highlight switched from bg-discord-text-link (a
   bright cyan that read as a hyperlink) to bg-discord-primary,
   matching the rest of the brand palette.

Also: SlashParamHint now keeps the `@botnick` suffix in the returned
cmdName so its schemas lookup tries `cmd@bot` first (specific) and
falls back to `cmd` (bare); ChatArea's schemas map writes both keys
per bot command.  Test for getActiveParamContext updated to match.

* slash picker: switch server-bot badge from purple to sky

The selected-row highlight is bg-discord-primary (blurple #5865F2),
and the server-bot badge was bg-discord-primary/30 + text-discord-
primary -- meaning the badge disappeared against its own selected
row.  Sky-700/40 + sky-300 reads as "network-wide service",
contrasts every other badge state, and stays legible on both the
default dark row and the selected blurple row.

* slash picker: open a typed param-form modal on select

When a bot publishes a command with declared options, picking it
from the slash popover now opens a one-per-option form modal
instead of putting the user on the hook to remember positional
arg ordering and free-form-type each value.

Renderer is type-driven from BotCommandOption.type:

  string             text input
  int / number       numeric input (step=1 / step=any)
  bool               checkbox
  user               text + datalist of channel members + DM partner
  channel            text + datalist of joined channels
  date / time /
  datetime           native HTML picker
  country            select w/ ISO-3166-1 flag + name; wire value is
                     the alpha-2 code (lib/countries.ts)
  password           masked text input
  any with choices[] select of the bot-declared choices (overrides
                     the type renderer)

useMessageSending grew an exported sendBotCommand() that takes a
pre-resolved bot + structured options map -- bypassing the
positional-arg parser used by the inline-typing path.

Translations for the 5 new strings added across all 18 non-English
locales.

* slash commands: reactive hint + invocation attribution on bot replies

Two related fixes around the slash-command UX:

1) The SlashParamHint stuck around after editing the command name to
   something the schemas didn't recognise, because the hint was
   rendered from messageTextRef.current (a ref, no re-render trigger)
   while the parent intentionally skips re-renders on every keystroke
   for perf.  Subscribe to the input element's events from inside the
   hint so it re-evaluates context independently and disappears as
   soon as the cmd-name no longer matches.

2) Public slash commands are sent as a TAGMSG, so other channel
   members only see the bot's reply with no context for what
   triggered it.  Render an attribution chip above the bot's reply
   when it carries the new +obby.world/invoked-by tag -- a base64
   JSON snapshot of {nick, name, options} produced server-side at
   dispatch time.

The server-side half of (2) lands in pushbot.c: PbInteraction gains
an invoker_cmd_b64 field built during pb_dispatch_command, and
pb_make_reply_tags appends +obby.world/invoked-by alongside the
existing +reply / +draft/channel-context tags.

* BotsModal: click commands to invoke; filter by reachability

Two related changes pair with the server-side discovery split:

1) Each slash command in BotDetail is now a button; clicking it
   opens SlashCommandParamModal with the bot + command pre-set.
   Submit dispatches via the existing sendBotCommand path, which
   routes by command.visibility (public -> current channel,
   private -> DM to the bot with channel-context).  BotsModal
   closes when a command is picked so the param modal isn't
   stacked underneath.

2) The bot list filters by reachability so the user only sees
   bots they can actually invoke right now: server-scope always,
   channel-scope only if any of bot.channels overlaps a channel
   they're a member of.  Opers bypass the filter for management.

Companion server-side change (pushbot.c) restricts the connect
burst to server-scope bots only; channel-scope arrives on the
relevant LOCAL_JOIN.  Opers still get every bot in the burst.

* bot-tools/bot-cmds: rework client to the draft/ IRCv3 Bot Tools spec

Combines the workflow-viewer (draft-ai-tools) and slash-command (pushbot-client)
features and aligns both with the IRCv3 "Bot Tools" draft spec.

bot-tools (workflow transparency):
- cap draft/bot-tools (was draft/ai-tools); tag +draft/bot-tools (was the vendor
  +obby.world/ai-tools)
- tag value is base64 of compact JSON (shared, UTF-8-safe src/lib/base64.ts)
- rename thinking -> reasoning and the steer action -> input; add the workflow
  features array and step cancelled-by

bot-cmds (slash commands):
- valueless +draft/bot-cmds-query (was =1)
- command schema uses contexts (public/private/pm) and requires, dropping the
  legacy visibility/scopes/version fields
- the invocation payload carries the target bot (public disambiguation) and the
  channel (private context) rather than relying on +draft/channel-context, which
  is not valid on TAGMSG
- base64 with padding via the shared util

i18n: the 46 new strings translated across all 18 non-English locales.

* bot-cmds: never route a private command publicly (privacy)

sendBotCommand defaulted a command with no `contexts` to ["public"], which made
private commands (e.g. /report) get sent as a public TAGMSG to the channel when
their schema arrived via the obby.world/bot-info directory (which carries the
legacy visibility/scopes, not contexts). resolveContexts now falls back to
visibility/scopes so a private/dm command is routed privately even before the
directory emits the spec `contexts`.

* irc/connection: surface ERROR + rateLimited as global notifications

Previously, IRC ERROR lines from the server were detected (handleError
logs + isRateLimitError matches /throttled/i), rateLimited fired, but
nothing in the React store or any component subscribed to the event.
Non-throttle ERROR lines were swallowed entirely after a console.log.

Now:
 - handleError also triggers a generic 'serverError' event for any ERROR
   that isn't a rate-limit, so the message reaches the store layer.
 - Store subscribes to both 'rateLimited' (warn) and 'serverError' (fail)
   and surfaces them via addGlobalNotification, picking up the existing
   GlobalNotifications.tsx toast UI.

* ircclient: surface transport-level connect failures via serverError

socket.onerror was rejecting with a generic "Failed to connect to host:port"
that swallowed the underlying transport error (Tauri native-tls handshake
failures, ConnectionRefused, etc.). Web WebSocket emits a typed event that's
roughly as opaque, but Tauri's invoke("connect") returns a real error string
("TLS handshake failed: ...", "Failed to connect: ...") that was being
thrown away.

Preserve the cause in the rejection AND fire a serverError event so the
notification subscriber surfaces a toast — symmetric with how IRC-protocol
ERROR lines are already handled.

* desktop: surface connect failures via timeouts + a raw-log viewer

Tauri's TCP/TLS connect path had no timeout, so a stalled handshake to
e.g. irc.mirc.club waited on kernel retransmits for ~2 min with no UI
feedback. Add 15s timeouts to both the TCP connect and the TLS
handshake; the timeout error string flows through socket.onerror, then
through serverError, then into a global notification toast.

Also add a raw IRC log viewer (Ctrl+Shift+L) that captures every TX/RX
line plus connect/error info lines into a 2000-line ring buffer per
server. Modal shows the current server's log with copy-all + clear.

Android manifest: declare CAMERA/RECORD_AUDIO/MODIFY_AUDIO_SETTINGS
plus optional camera/microphone hardware features so Tauri's
RustWebChromeClient's runtime permission launcher actually has perms
to grant.

* desktop: fix port-double in AddServerModal + wrong IRC defaults in parser

Two compounding bugs put mirc.club on ircs://irc.mirc.club:443 instead
of :6697 (which is nginx's HTTPS port, hence the HTTP 400 reply):

1. AddServerModal stripped the scheme from the host string before
   appending the user-entered port, but did NOT strip the embedded
   :port / path. The discovery prefill passes host="ircs://h:6697",
   port="6697", so we built ircs://h:6697:6697 — a triple-colon URL.

2. ircUrlParser's fallback (for malformed URLs the regex can't parse)
   used the WebSocket defaults — 443 for ircs:// and 8000 for irc://
   — instead of the IRC standard ports 6697 / 6667. So the malformed
   triple-colon URL hit the fallback and silently became port 443.

Strip everything after the scheme in cleanHost (host part only), and
fix the IRC fallback ports. Existing broken entries auto-recover the
next time the parser falls through.

* bot-tools: address CodeRabbit findings on PR #238

Picked the findings that were genuine correctness or HTML-validity
issues; skipped the lingui-extraction nags (those land in a separate
i18n pass via lefthook).

- BotInvocationChip: switch the inline base64 decode to base64DecodeUtf8
  so emoji and non-ASCII bot names round-trip correctly (atob alone is
  Latin-1).

- aiTools.decodeAiToolsValue: reject unknown enum values for workflow
  state, step type, step state, and action type. The function contract
  said schema mismatches return null, but obj.state etc. were cast
  through without checking against the type unions.

- ChatArea slash-suggestion fanout: bot scope filter looked up botNick
  in a lowercased chanUsers set without lowercasing the bot's own nick,
  so mixed-case bot nicks always reported "not in channel". Also let
  channel-scoped bots leak into PM context (selectedChannel was used to
  bypass the membership check); change to require a channel + membership.
  Both 2410-block and the 2543-block.

- BotsModal search filter: guard b.realname with ?? "" so a bot whose
  RPL_WHOIS lacked the realname field doesn't blow up on toLowerCase.

- useMessageSending.sendBotCommand: only dispatch the PM fallback when
  the command actually declares "pm" in contexts. The old final else
  fired even for context-restricted commands, sending an invocation the
  bot would reject anyway.

- SlashCommandPopover: the popover is position:fixed so
  getBoundingClientRect().left is already viewport-relative; the extra
  + window.scrollX shifted the anchor on scrolled pages.

- AiToolsCard header: the outer was a <button> and contained nested
  Dismiss/Cancel <button>s, which is invalid HTML and reportedly breaks
  focus/click on some platforms. Convert the outer to a <div
  role="button"> with keyboard handler; the inner buttons stay native.
  Biome a11y/useSemanticElements is suppressed at that one spot since
  using a <button> there is exactly what we're avoiding.

- AiToolsCard countdown effect: drop the Zustand action ref `dismiss`
  from the deps and apply the project's standard biome-ignore for
  useExhaustiveDependencies (per CLAUDE.md).

* chore(tauri/unifiedpush): drop committed Gradle build outputs + Cargo.lock

Matt flagged the build artifacts under android/build/ (lint XML referring
to local paths, kotlin compile-cache binaries, manifest-merger logs) and
the plugin's Cargo.lock.

Remove 148 files (147 from android/build/, 1 Cargo.lock) and add a
.gitignore so `tauri android build` doesn't re-track them. Cargo.lock is
omitted per Rust library convention (apps commit lockfiles, libraries
don't). android/.tauri/ stays tracked to match the ios-keyboard plugin
precedent — those are auto-generated tauri-api stubs, not build outputs.

* bot-tools: address matt's review on PR #238

* Rename AiTools -> BotTools across the codebase. The wire format is
  draft/bot-tools and matt rightly pointed out the internal naming was
  out of sync with the spec. 9 files renamed (src/lib/{aiTools->botTools}
  .ts, src/store/handlers/{aiTools->botTools}.ts, the five Ai* components,
  both Ai* test files) plus 66+ symbol references (AiTools->BotTools,
  aiTools->botTools, AI_TOOLS_->BOT_TOOLS_) updated transitively. Wire-
  format strings ("+draft/bot-tools", "draft/bot-tools") stay literal.

* i18n the client slash-command labels in clientCommands.ts. Module-
  scope `t` is unsafe (catalogue isn't activated at import time --
  CLAUDE.md), so the CLIENT_COMMANDS const becomes a getClientCommands()
  function that re-evaluates each call. CLIENT_COMMAND_NAMES stays a
  locale-independent Set. 13 new strings translated across all 18
  non-English locales in parallel.

* SlashCommandPopover: refuse to render until inputRect has a real
  position. Previously the first frame after `isVisible` flipped true
  could land before the inputElement ref had resolved, so the popover
  rendered at the (100, 100) fallback anchor for one frame -- a visible
  flash up-and-left of the input that matt asked about.

The unifiedpush build-artifact cleanup landed in the previous commit.

* chore(tauri): drop the unifiedpush plugin from this branch entirely

Matt's right that this doesn't belong here. The whole
src-tauri/plugins/unifiedpush/ tree got picked up by accident in
fc50a0f ('desktop: surface connect failures...'); my working tree had
the parallel feature/webpush branch's scaffolding sitting in the same
src-tauri/plugins/ directory and `git add -A` swept it in.

Nothing on this branch actually wires the plugin in -- no references
in src-tauri/src/, Cargo.toml, tauri.conf.json, or anywhere in src/.
It's orphan scaffolding. The plugin lives on feature/webpush (PR #236)
where it's actually built into the app.

Removed all 30 remaining files (the previous chore commit only got
android/build/ + Cargo.lock, 148 files; this clears the rest -- the
android/.tauri/ tauri-api stubs and the empty plugin shell).

Build + 845 tests still green; nothing on this branch was depending on it.

* bot-tools: i18n SlashParamHint literals + fi 'offline'

Two CodeRabbit findings, both still valid against the merged tree:

* SlashParamHint left four user-visible literals raw in JSX:
  "via @${botNick}", "(handled by ObsidianIRC)", "required", "one of:".
  Wrap each with <Trans>, route the bot-nick through Trans's
  interpolation so the placeholder reaches the catalogue. Skipped the
  "string" type literal (it's a schema-data identifier, not prose) and
  the option name/description spans (those are bot-supplied strings,
  not translatable from this side).

* fi/messages.po had "offline" -> "offline" (English fallback) on the
  BotsModal status badge. Change to "ei verkossa".

Re-extract pulled in the 4 new strings; translated all 18 non-English
locales inline. .mjs recompiled, 858 tests + build green.

* bot-tools: spec-compliant draft/bot-cmds batch reassembly + invoked-by rename

- pushbot handler: on BATCH_START with type=draft/bot-cmds, allocate
  a fragments buffer keyed by (serverId, batchRef). Each in-batch
  TAGMSG with +draft/bot-cmds appends its raw base64 fragment to the
  buffer instead of decoding individually. On BATCH_END, concatenate,
  base64-decode, JSON-parse, and commit. Matches the bot-tools spec's
  'split across batch messages, concatenated before decoding' shape.

- Rename +obby.world/invoked-by -> +draft/invoked-by in the bot
  invocation chip and the MessageItem tag lookup. The spec now
  defines this tag under draft/.

* bot-tools: carry sender on BATCH_START so draft/bot-cmds chunks key by bot

The draft/bot-cmds spec uses BATCH +ref draft/bot-cmds <target>, where
the target on the open line is the asker, not the sender. The sender
nick lives in the BATCH command's source prefix; without exposing it
on the BATCH_START event the pushbot handler stored each batch under
an empty-string key and could never write the reassembled list back
to the right bot's botCommands entry.

* bot-tools: console.log BATCH start/end + decode result for live debugging

* bot-tools: synthesise PushBotInfo so draft/bot-cmds bots show in modal

* bot-tools: surface draft/bot-cmds-loading bots in header + modal

* bot-tools: visible loading dot on inline bot button, channel-scope synth, z-index

* bot-tools: portal modal to root + synthesise placeholder bot on BATCH_START

* bot-tools: render BotInvocationChip on ACTION (/me) bot replies too

MessageItem.tsx branches off into ActionMessage.tsx for any message
whose content starts with \\u0001ACTION, and that path was rendered
without the chip -- so every bot whose reply was a /me (e.g. 8ball's
'Cloudia shakes the magic 8 ball...') showed up with no attribution
even when +draft/invoked-by was on the wire. The non-action PRIVMSG
branch in MessageItem.tsx already rendered it; mirror that import +
placement in ActionMessage.tsx above the italic body so the user
sees who triggered the action and with what args.

* bot-tools: address mattf's PR #238 review — close 3 security/UX holes

* bot-tools: filter privileged command names from non-config bots + info popover on chip

* bot-tools: chathistory spam fix + JOIN discovery + Bot Commands submenu

* bot-tools: move Bot Commands list into the member-list context-menu submenu

* bot-tools: portal the Bot Commands submenu so the parent menu's overflow doesn't clip it

* UserContextMenu: solid sticky header on Bot Commands submenu, neutral text color

Two cosmetic fixes on the flyout's 'Show in Bots Menu' row:

- Sticky positioning was on the button itself, but `sticky` only
  pins the element's content box, not the gap consumed by the
  parent's vertical padding (`py-1` on the menu).  Command rows
  scrolled visibly through the sliver below the button.  Wrap the
  button in a sticky <div> with z-10 and put the bg + border on
  that wrapper so the entire pinned strip is opaque.

- text-discord-blue rendered dark navy on the dark-300 background;
  the rest of the menu uses text-discord-text-normal with hover
  text-white.  Switch to that pair for visual consistency.

* UserContextMenu: drop top padding on Bot Commands submenu so sticky header sits flush

The submenu container had `py-1` -- 4px top + 4px bottom padding.
sticky top-0 anchors the header below the top padding, so 4px of
the first command row peeked through that transparent strip while
scrolling.  Swap `py-1` for `pb-1` so the header is flush with
the top border and there's no peek-through; bottom still has its
breathing room.

* bot-tools: shared LoadingSpinner + 'Commands are loading…' empty-state

* bot-tools: drop auto-pop tray, accumulating tool pills, two-tier history dropdown

* bot-tools: 10-min timeout, always-on close, wrapping pills with k=v param chips

* BotToolsPlaceholderBody: drop array-index key in param-pair list

Biome's noArrayIndexKey lint flagged the pair-rendering map.  All
three paramPairs paths produce a non-empty p.key already (object
entries use the property name; array entries use the index as the
key field; the primitive/string path emits a single pair with an
empty key, which has no sibling to collide with).  Use that as the
React key instead of the array index.

* i18n: re-extract + compile after main merge

* i18n: wrap residual user-visible strings + restructure module-scope badge maps

Audit of PR #238 surface (workflow + bot-cmds UI) found 30 unwrapped
English strings.  All four scouts converged on two patterns:

1. Module-scope label maps (STATUS_BADGE / SCOPE_BADGE / FILTER_LABELS
   in BotsModal.tsx; badgeStyle() in SlashCommandPopover.tsx).  These
   evaluate before i18n.activate so any t-template invoked at module
   init returns the source key, not the active locale's string.  Move
   the colour-class records to module scope (locale-invariant) and
   wrap labels + tooltips in per-render hooks (useStatusBadge,
   useScopeBadge, useFilterLabels) / functions that take t as an
   argument (badgeStyle(source, t)).

2. Direct unwrapped literals: 'active', 'timed out', 'tool' (the
   fallback display name for an anonymous step in
   BotToolsPlaceholderBody), 'required', 'nick' / '#channel'
   placeholders in SlashCommandParamModal, 'Slash commands' header,
   'Play Tic-Tac-Toe' in ChatHeader (label + aria-label + title), and
   the 'X seconds/minutes/hours/days' template literals in
   UserProfileModal's formatIdleTime.

Also wraps the effective workflow state ('complete' / 'failed' /
'cancelled' / 'timed out') in BotToolsHistoryButton so the row's
'· complete' style suffix doesn't leak English into non-English
UIs.

Tests/lint clean; catalogs recompiled via i18n:extract + i18n:compile.
The diff includes the auto-generated locale .po + .mjs updates so the
next merge into main doesn't reintroduce stale msgid drift.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants