diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index 4b0f1cce6..d6b36681c 100644 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -354,6 +354,34 @@ Context properties: * `message`: The message object that will be sent to the Etherpad server. +## `chatPrefillFromUser` + +Called from: `src/static/js/pad_userlist.ts` + +Called when the user clicks an entry in the user list. The default behavior is to +open the chat panel and prefill the input with `@ `, where `` is that +user's display name (with whitespace replaced by underscores). Plugins can return +a different prefill string from their callback — the first non-empty string +returned wins. + +Typical use is by AI/bot plugins whose author display name (e.g. "AI Assistant") +isn't a useful @-mention; the plugin can substitute its trigger string instead. + +Context properties: + +* `authorId`: The clicked user's author id. +* `name`: The clicked user's display name. +* `prefill`: The default prefill string Etherpad would otherwise use. + +Example: + +```javascript +exports.chatPrefillFromUser = (hookName, {authorId, name}, cb) => { + if (authorId === window.clientVars.ep_my_bot.authorId) return cb('@bot '); + return cb(); +}; +``` + ## collectContentPre Called from: `src/static/js/contentcollector.js` diff --git a/src/static/js/pad_userlist.ts b/src/static/js/pad_userlist.ts index 85bb32a98..639da8b0f 100644 --- a/src/static/js/pad_userlist.ts +++ b/src/static/js/pad_userlist.ts @@ -19,6 +19,7 @@ import padutils from './pad_utils' const hooks = require('./pluginfw/hooks'); +const chat = require('./chat').chat; import html10n from './vendors/html10n'; let myUserInfo = {}; @@ -369,6 +370,46 @@ const paduserlist = (() => { }, 0); }); + // Click any other user's row to open chat with @ prefilled. + // Helps newcomers discover the chat panel and the @-mention convention + // without having to be told. Plugins can transform the prefilled text + // — for example ep_ai_chat replaces "@AI Assistant" with the + // configured trigger ("@ai") — via the chatPrefillFromUser client + // hook (see doc/api/hooks_client-side.md). + $('#otheruserstable').on('click', 'tr[data-authorId]', async function (event) { + // Skip clicks on the color swatch — that has its own click handler + // (color-picker semantics) and shouldn't double up as a chat trigger. + if ($(event.target).closest('.usertdswatch').length) return; + const tr = $(this); + const authorId = tr.attr('data-authorId'); + if (!authorId) return; + const name = (tr.find('.usertdname').text() || '').trim(); + let prefill = name ? `@${name.replace(/\s+/g, '_')} ` : ''; + try { + const transforms = await hooks.aCallAll( + 'chatPrefillFromUser', {authorId, name, prefill}); + if (Array.isArray(transforms)) { + for (const tr2 of transforms) { + if (typeof tr2 === 'string' && tr2.length > 0) { prefill = tr2; break; } + } + } + } catch { /* never let a misbehaving plugin break the click */ } + try { chat.show(); } catch { /* */ } + setTimeout(() => { + const $input = $('#chatinput'); + if (!$input.length) return; + const current = ($input.val() || '') as string; + if (!current.trim() || /^@\S+\s*$/.test(current.trim())) { + $input.val(prefill); + } else if (!current.includes(prefill.trim())) { + $input.val(`${current.trimEnd()} ${prefill}`); + } + $input.trigger('focus'); + const elem = $input[0] as HTMLTextAreaElement; + try { elem.setSelectionRange(elem.value.length, elem.value.length); } catch (_e) { /* */ } + }, 50); + }); + // color picker $('#myswatchbox').on('click', showColorPicker); $('#mycolorpicker .pickerswatchouter').on('click', function () { diff --git a/src/static/skins/colibris/src/components/users.css b/src/static/skins/colibris/src/components/users.css index c04453c57..c15f984e6 100644 --- a/src/static/skins/colibris/src/components/users.css +++ b/src/static/skins/colibris/src/components/users.css @@ -2,6 +2,19 @@ table#otheruserstable { margin-top: 20px; } +/* + * Clicking a row in the user list opens chat and prefills "@ ". + * The pointer cursor on the name cell makes the affordance visible — + * the swatch keeps its own (color-picker) click semantics, so leave + * its cursor alone. + */ +#otheruserstable tr[data-authorId] .usertdname { + cursor: pointer; +} +#otheruserstable tr[data-authorId]:hover .usertdname { + text-decoration: underline; +} + .popup#users.chatAndUsers > .popup-content { padding: 20px 10px; height: 250px;