From 3c252d4b62090819f4dc75a99288ababdbf6c152 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Wed, 13 May 2026 15:35:14 -0500 Subject: [PATCH 1/4] reel: gap-fill lure metadata on describe, use group title in og fallback Two related fixes to lure open-graph metadata. %reel-describe now backfills missing fields from server state when the caller's describe is sparse: title/description/image scried from %groups, and nickname/avatar from our cached profile. Caller-provided values always win, and scry failures are tolerated via mole. This lets pioneer create lures with a bare describe and still get a fully populated preview, instead of relying on later subscription updates. The "a Groupchat" fallback in the og-title formula was unconditional in the no-nickname branch, ignoring a real group title when one was present. It now uses group-title (which itself still falls back to "a Groupchat" when the title is empty), so a titled group renders "You're Invited to " instead of "You're Invited to a Groupchat". Tests gain a scry mock returning an empty group:v9:gv for the gap-fill path, and the test-groups-update expectation is updated to reflect the corrected fallback behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- desk/app/reel.hoon | 55 ++++++++++++++++++++++++++++++++++++++-- desk/tests/app/reel.hoon | 17 +++++++++++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/desk/app/reel.hoon b/desk/app/reel.hoon index b62db9a8be..9ba5998f87 100644 --- a/desk/app/reel.hoon +++ b/desk/app/reel.hoon @@ -230,6 +230,57 @@ [%'inviterUserId' (scot %p src.bowl)] [%'invitedGroupId' id] == + :: gap-fill open-graph metadata from server state when the caller + :: didn't provide it: title / description / image from %groups, + :: and nickname / avatar from our cached profile. Caller-provided + :: values always win; scry failures are tolerated. + :: + =/ type=(unit @t) (~(get by fields.metadata) %'inviteType') + =? fields.metadata |(?=(~ type) =('group' u.type)) + =/ fld fields.metadata + :: inviter fields from our-profile + :: + =/ nickname=(unit @t) (~(get cy:t our-profile) %nickname %text) + =/ avatar=(unit @t) (~(get cy:t our-profile) %avatar %look) + =? fld + ?& ?=(^ nickname) + !=('' u.nickname) + =(~ (~(get by fld) %'inviterNickname')) + == + (~(put by fld) %'inviterNickname' u.nickname) + =? fld + ?& ?=(^ avatar) + !=('' u.avatar) + =(~ (~(get by fld) %'inviterAvatarImage')) + == + (~(put by fld) %'inviterAvatarImage' u.avatar) + :: group meta from %groups; only attempt when id parses as a flag + :: + =/ parsed=(unit flag:v0:groups-ver) (rush id flag) + ?~ parsed fld + =/ grp=(unit group:v9:groups-ver) + %- mole |. + .^ group:v9:groups-ver %gx + /(scot %p our.bowl)/groups/(scot %da now.bowl)/v2/groups/(scot %p p.u.parsed)/[q.u.parsed]/group-2 + == + ?~ grp fld + =* gmeta meta.u.grp + =? fld + ?& !=('' title.gmeta) + =(~ (~(get by fld) %'invitedGroupTitle')) + == + (~(put by fld) %'invitedGroupTitle' title.gmeta) + =? fld + ?& !=('' description.gmeta) + =(~ (~(get by fld) %'invitedGroupDescription')) + == + (~(put by fld) %'invitedGroupDescription' description.gmeta) + =? fld + ?& !=('' image.gmeta) + =(~ (~(get by fld) %'invitedGroupIconImageUrl')) + == + (~(put by fld) %'invitedGroupIconImageUrl' image.gmeta) + fld :: the nonce here is a temporary identifier for the metadata. :: a new one will be assigned by the bait provider and returned to us. :: @@ -402,7 +453,7 @@ =/ title=@t %- crip ?: |(?=(~ nickname) =('' u.nickname)) - "Tlon Messenger: You're Invited to a Groupchat" + "Tlon Messenger: You're Invited to {(trip group-title)}" "Tlon Messenger: {(trip u.nickname)} invited you to {(trip group-title)}" =. fields.update (~(put by fields.update) %'$og_title' title) @@ -455,7 +506,7 @@ =/ title=@t %- crip ?: |(?=(~ nickname) =('' u.nickname)) - "Tlon Messenger: You're Invited to a Groupchat" + "Tlon Messenger: You're Invited to {(trip group-title)}" "Tlon Messenger: {(trip u.nickname)} invited you to {(trip group-title)}" %- ~(gas by *(map field:reel cord)) :~ %'invitedGroupTitle'^title.meta diff --git a/desk/tests/app/reel.hoon b/desk/tests/app/reel.hoon index 131ad84cda..5c8f5e8747 100644 --- a/desk/tests/app/reel.hoon +++ b/desk/tests/app/reel.hoon @@ -46,6 +46,16 @@ %- ~(gas by *contact:t) :~ %nickname^text+'Sampel Palnet' == +:: scry mock for the gap-fill path in %reel-describe. returns an empty +:: group so the agent's gap-fill checks (!=('' title.gmeta) etc.) all +:: fall through and no fields are added. +:: +++ scry + |= =(pole knot) + ?+ pole ~|(`path`pole !!) + [%gx @ %groups @ %v2 %groups host=@ term=@ %group-2 ~] + `!>(*group:v9:gv) + == ++ do-register-invite |= [=token:reel =metadata:reel] =/ m (mare ,(list card)) @@ -134,6 +144,7 @@ ^- form:m ;< * bind:m (do-init dap reel-agent) ;< ~ bind:m (jab-bowl |=(=bowl bowl(now ~2025.9.3, our ~sampel-palnet))) + ;< ~ bind:m (set-scry-gate scry) :: a group invite can be requested from reel :: =/ =nonce:reel (scot %da ~2025.9.3) @@ -186,6 +197,7 @@ ^- form:m ;< ~ bind:m (jab-bowl |=(=bowl bowl(our ~sampel-palnet))) ;< caz=(list card) bind:m (do-init dap reel-agent) + ;< ~ bind:m (set-scry-gate scry) ;< * bind:m (do-agent /contacts [~sampel-palnet %contacts] %watch-ack ~) ;< * bind:m (do-agent /groups [~sampel-palnet %groups] %watch-ack ~) ;< * bind:m (do-register-invite ~.0v1 group-invite-meta) @@ -214,8 +226,8 @@ :~ %'invitedGroupTitle'^'Early Sunrise' %'invitedGroupDescription'^'Sunrise, sunset.' %'invitedGroupIconImageUrl'^'https://sampel-palnet.arvo.network/early-sunrise.jpg' - %'$og_title'^'Tlon Messenger: You\'re Invited to a Groupchat' - %'$twitter_title'^'Tlon Messenger: You\'re Invited to a Groupchat' + %'$og_title'^'Tlon Messenger: You\'re Invited to Early Sunrise' + %'$twitter_title'^'Tlon Messenger: You\'re Invited to Early Sunrise' == ;< ~ bind:m %+ ex-cards caz @@ -294,6 +306,7 @@ ^- form:m ;< ~ bind:m (jab-bowl |=(=bowl bowl(our ~sampel-palnet))) ;< caz=(list card) bind:m (do-init dap reel-agent) + ;< ~ bind:m (set-scry-gate scry) ;< * bind:m (do-agent /contacts [~sampel-palnet %contacts] %watch-ack ~) ;< * bind:m (do-register-invite ~.0v1 group-invite-meta) ;< * bind:m (do-register-invite ~.0v2 personal-invite-meta) From 593870ef93511fa6c41fa3e9492bb70c681251f0 Mon Sep 17 00:00:00 2001 From: Hunter Miller <hunter@hmiller.dev> Date: Wed, 13 May 2026 16:17:24 -0500 Subject: [PATCH 2/4] reel: extract group-og-title helper Two call sites in the /contacts and /groups subscription handlers were computing the same group-invite open-graph title. Extracted the formula (including the "a Groupchat" fallback) into a single helper in the upper |% core. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- desk/app/reel.hoon | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/desk/app/reel.hoon b/desk/app/reel.hoon index 9ba5998f87..591486dac1 100644 --- a/desk/app/reel.hoon +++ b/desk/app/reel.hoon @@ -117,6 +117,19 @@ |* [caz=(list card) etc=*] [[cad caz] etc] -- +:: +group-og-title: render the open-graph title for a group invite. +:: empty group title falls back to "a Groupchat". +:: +++ group-og-title + |= [nickname=(unit @t) group-title=@t] + ^- @t + =/ gt=@t + ?: =('' group-title) 'a Groupchat' + group-title + %- crip + ?: |(?=(~ nickname) =('' u.nickname)) + "Tlon Messenger: You're Invited to {(trip gt)}" + "Tlon Messenger: {(trip u.nickname)} invited you to {(trip gt)}" -- |_ =bowl:gall +* this . @@ -445,16 +458,9 @@ :: =+ type=(~(get by fields.meta) %'inviteType') ?: |(?=(~ type) =('group' u.type)) - =/ group-title=@t - =+ til=(~(get by fields.meta) %'invitedGroupTitle') - ?: |(?=(~ til) =('' u.til)) - 'a Groupchat' - u.til =/ title=@t - %- crip - ?: |(?=(~ nickname) =('' u.nickname)) - "Tlon Messenger: You're Invited to {(trip group-title)}" - "Tlon Messenger: {(trip u.nickname)} invited you to {(trip group-title)}" + %+ group-og-title nickname + (fall (~(get by fields.meta) %'invitedGroupTitle') '') =. fields.update (~(put by fields.update) %'$og_title' title) =. fields.update @@ -499,15 +505,9 @@ ?+ -.r-group.r-groups ~ %meta =* meta meta.r-group.r-groups - =+ nickname=(~(get cy:t our-profile) %nickname %text) - =/ group-title=@t - ?: =('' title.meta) 'a Groupchat' - title.meta =/ title=@t - %- crip - ?: |(?=(~ nickname) =('' u.nickname)) - "Tlon Messenger: You're Invited to {(trip group-title)}" - "Tlon Messenger: {(trip u.nickname)} invited you to {(trip group-title)}" + %- group-og-title + [(~(get cy:t our-profile) %nickname %text) title.meta] %- ~(gas by *(map field:reel cord)) :~ %'invitedGroupTitle'^title.meta %'invitedGroupDescription'^description.meta From 869b36f601ee1d6d173e49b4d3d534be5c8eda72 Mon Sep 17 00:00:00 2001 From: Hunter Miller <hunter@hmiller.dev> Date: Thu, 14 May 2026 16:58:40 -0500 Subject: [PATCH 3/4] reel: switch fake moles for %u --- desk/app/reel.hoon | 31 ++++++++++++++++++++----------- desk/tests/app/reel.hoon | 10 +++++++--- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/desk/app/reel.hoon b/desk/app/reel.hoon index 591486dac1..72ff657dbd 100644 --- a/desk/app/reel.hoon +++ b/desk/app/reel.hoon @@ -246,11 +246,18 @@ :: gap-fill open-graph metadata from server state when the caller :: didn't provide it: title / description / image from %groups, :: and nickname / avatar from our cached profile. Caller-provided - :: values always win; scry failures are tolerated. + :: values always win. :: =/ type=(unit @t) (~(get by fields.metadata) %'inviteType') =? fields.metadata |(?=(~ type) =('group' u.type)) =/ fld fields.metadata + :: treat absent or empty-string fields as gap-fillable. + :: + =/ blank + |= [m=(map cord cord) k=cord] + ^- ? + =/ v (~(get by m) k) + ?|(?=(~ v) =('' u.v)) :: inviter fields from our-profile :: =/ nickname=(unit @t) (~(get cy:t our-profile) %nickname %text) @@ -258,39 +265,41 @@ =? fld ?& ?=(^ nickname) !=('' u.nickname) - =(~ (~(get by fld) %'inviterNickname')) + (blank fld %'inviterNickname') == (~(put by fld) %'inviterNickname' u.nickname) =? fld ?& ?=(^ avatar) !=('' u.avatar) - =(~ (~(get by fld) %'inviterAvatarImage')) + (blank fld %'inviterAvatarImage') == (~(put by fld) %'inviterAvatarImage' u.avatar) :: group meta from %groups; only attempt when id parses as a flag + :: and the group exists locally. :: =/ parsed=(unit flag:v0:groups-ver) (rush id flag) ?~ parsed fld - =/ grp=(unit group:v9:groups-ver) - %- mole |. + =/ base /(scot %p our.bowl)/groups/(scot %da now.bowl) + ?. .^(? %gu (weld base /groups/(scot %p p.u.parsed)/[q.u.parsed])) + fld + =/ grp=group:v9:groups-ver .^ group:v9:groups-ver %gx - /(scot %p our.bowl)/groups/(scot %da now.bowl)/v2/groups/(scot %p p.u.parsed)/[q.u.parsed]/group-2 + (weld base /v2/groups/(scot %p p.u.parsed)/[q.u.parsed]/group-2) == - ?~ grp fld - =* gmeta meta.u.grp + =* gmeta meta.grp =? fld ?& !=('' title.gmeta) - =(~ (~(get by fld) %'invitedGroupTitle')) + (blank fld %'invitedGroupTitle') == (~(put by fld) %'invitedGroupTitle' title.gmeta) =? fld ?& !=('' description.gmeta) - =(~ (~(get by fld) %'invitedGroupDescription')) + (blank fld %'invitedGroupDescription') == (~(put by fld) %'invitedGroupDescription' description.gmeta) =? fld ?& !=('' image.gmeta) - =(~ (~(get by fld) %'invitedGroupIconImageUrl')) + (blank fld %'invitedGroupIconImageUrl') == (~(put by fld) %'invitedGroupIconImageUrl' image.gmeta) fld diff --git a/desk/tests/app/reel.hoon b/desk/tests/app/reel.hoon index 5c8f5e8747..581c7b81f2 100644 --- a/desk/tests/app/reel.hoon +++ b/desk/tests/app/reel.hoon @@ -46,13 +46,17 @@ %- ~(gas by *contact:t) :~ %nickname^text+'Sampel Palnet' == -:: scry mock for the gap-fill path in %reel-describe. returns an empty -:: group so the agent's gap-fill checks (!=('' title.gmeta) etc.) all -:: fall through and no fields are added. +:: scry mock for the gap-fill path in %reel-describe. the %gu check +:: reports the group as present so the %gx fetch proceeds; the %gx +:: returns an empty group so the agent's gap-fill checks +:: (!=('' title.gmeta) etc.) all fall through and no fields are added. :: ++ scry |= =(pole knot) ?+ pole ~|(`path`pole !!) + [%gu @ %groups @ %groups host=@ term=@ ~] + `!>(&) + :: [%gx @ %groups @ %v2 %groups host=@ term=@ %group-2 ~] `!>(*group:v9:gv) == From fae39252f2e4c02ec9bbf1fae57b722266abadfd Mon Sep 17 00:00:00 2001 From: Hunter Miller <hunter@hmiller.dev> Date: Mon, 18 May 2026 11:34:20 -0500 Subject: [PATCH 4/4] reel: address gap-fill style feedback Rename helpers (blank -> is-blank, gt -> title, gmeta -> meta, fld -> fields), tighten is-blank's key to field:reel, and switch the gap-fill block to =* aliasing so is-blank re-evaluates against the current fields binding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- desk/app/reel.hoon | 69 ++++++++++++++++++++-------------------- desk/tests/app/reel.hoon | 4 --- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/desk/app/reel.hoon b/desk/app/reel.hoon index 72ff657dbd..3708d0d4ae 100644 --- a/desk/app/reel.hoon +++ b/desk/app/reel.hoon @@ -123,13 +123,13 @@ ++ group-og-title |= [nickname=(unit @t) group-title=@t] ^- @t - =/ gt=@t + =/ title=@t ?: =('' group-title) 'a Groupchat' group-title %- crip ?: |(?=(~ nickname) =('' u.nickname)) - "Tlon Messenger: You're Invited to {(trip gt)}" - "Tlon Messenger: {(trip u.nickname)} invited you to {(trip gt)}" + "Tlon Messenger: You're Invited to {(trip title)}" + "Tlon Messenger: {(trip u.nickname)} invited you to {(trip title)}" -- |_ =bowl:gall +* this . @@ -245,64 +245,63 @@ == :: gap-fill open-graph metadata from server state when the caller :: didn't provide it: title / description / image from %groups, - :: and nickname / avatar from our cached profile. Caller-provided - :: values always win. + :: and nickname / avatar from our cached profile. caller-provided + :: values take priority. :: =/ type=(unit @t) (~(get by fields.metadata) %'inviteType') =? fields.metadata |(?=(~ type) =('group' u.type)) - =/ fld fields.metadata - :: treat absent or empty-string fields as gap-fillable. + =* fields fields.metadata + :: treat absent or empty-string fields as gap-fillable :: - =/ blank - |= [m=(map cord cord) k=cord] + =* is-blank + |= k=field:reel ^- ? - =/ v (~(get by m) k) + =/ v (~(get by fields) k) ?|(?=(~ v) =('' u.v)) :: inviter fields from our-profile :: =/ nickname=(unit @t) (~(get cy:t our-profile) %nickname %text) =/ avatar=(unit @t) (~(get cy:t our-profile) %avatar %look) - =? fld + =? fields ?& ?=(^ nickname) !=('' u.nickname) - (blank fld %'inviterNickname') + (is-blank %'inviterNickname') == - (~(put by fld) %'inviterNickname' u.nickname) - =? fld + (~(put by fields) %'inviterNickname' u.nickname) + =? fields ?& ?=(^ avatar) !=('' u.avatar) - (blank fld %'inviterAvatarImage') + (is-blank %'inviterAvatarImage') == - (~(put by fld) %'inviterAvatarImage' u.avatar) + (~(put by fields) %'inviterAvatarImage' u.avatar) :: group meta from %groups; only attempt when id parses as a flag :: and the group exists locally. :: - =/ parsed=(unit flag:v0:groups-ver) (rush id flag) - ?~ parsed fld + ?~ parsed=(rush id flag) fields =/ base /(scot %p our.bowl)/groups/(scot %da now.bowl) - ?. .^(? %gu (weld base /groups/(scot %p p.u.parsed)/[q.u.parsed])) - fld + ?. .^(? %gu (weld base /groups/(scot %p -.u.parsed)/[+.u.parsed])) + fields =/ grp=group:v9:groups-ver .^ group:v9:groups-ver %gx - (weld base /v2/groups/(scot %p p.u.parsed)/[q.u.parsed]/group-2) + (weld base /v2/groups/(scot %p -.u.parsed)/[+.u.parsed]/group-2) == - =* gmeta meta.grp - =? fld - ?& !=('' title.gmeta) - (blank fld %'invitedGroupTitle') + =* meta meta.grp + =? fields + ?& !=('' title.meta) + (is-blank %'invitedGroupTitle') == - (~(put by fld) %'invitedGroupTitle' title.gmeta) - =? fld - ?& !=('' description.gmeta) - (blank fld %'invitedGroupDescription') + (~(put by fields) %'invitedGroupTitle' title.meta) + =? fields + ?& !=('' description.meta) + (is-blank %'invitedGroupDescription') == - (~(put by fld) %'invitedGroupDescription' description.gmeta) - =? fld - ?& !=('' image.gmeta) - (blank fld %'invitedGroupIconImageUrl') + (~(put by fields) %'invitedGroupDescription' description.meta) + =? fields + ?& !=('' image.meta) + (is-blank %'invitedGroupIconImageUrl') == - (~(put by fld) %'invitedGroupIconImageUrl' image.gmeta) - fld + (~(put by fields) %'invitedGroupIconImageUrl' image.meta) + fields :: the nonce here is a temporary identifier for the metadata. :: a new one will be assigned by the bait provider and returned to us. :: diff --git a/desk/tests/app/reel.hoon b/desk/tests/app/reel.hoon index 581c7b81f2..56fd875978 100644 --- a/desk/tests/app/reel.hoon +++ b/desk/tests/app/reel.hoon @@ -46,10 +46,6 @@ %- ~(gas by *contact:t) :~ %nickname^text+'Sampel Palnet' == -:: scry mock for the gap-fill path in %reel-describe. the %gu check -:: reports the group as present so the %gx fetch proceeds; the %gx -:: returns an empty group so the agent's gap-fill checks -:: (!=('' title.gmeta) etc.) all fall through and no fields are added. :: ++ scry |= =(pole knot)