Handoff — reply Like + the resolved try-reset card
Product / feature change (one line)
In the group feed, the member an event is ABOUT can now acknowledge the replies under it with a one-word Like, and a gifted try-reset now resolves the original request card in place instead of leaving an open ask on screen.
Component changes
- Feed — the reply Like (
components/Feed/Feed.{html,js,css}, commit7244196) — a reply now carries exactly ONE control: a thumbs-up mark plus one word under the bubble, whose label IS the state. No separate "who liked" surface — the control and the attribution are the same node.- Only the event's owner may like. You acknowledge the replies under YOUR medal and nobody else's. Consequences the backend must honour: a reply has at most one liker, that liker is always the event's author, and "You and Chen W. liked" cannot occur.
- Three renderings, all
data-field="reply.likedByOwner"+data-formatted(the wording lives inlikeLabel(), not in the model):- my event, unliked →
Like— a<button> - my event, liked →
Liked— a<button>, interactive ink - someone else's →
Priya R. liked— a read-only<span>, and not rendered at all while the owner has not liked
- my event, unliked →
- Which events carry it: the PERSON-DIRECTED ones only —
medal-earned(→ the earner) andmember-joined(→ the joiner). A group-directed milestone (half-target,goal-reached, …) is addressed to the room, owns no acknowledger, and its replies carry no like node at all. Same split the VOCAB table draws. - New JS:
likeLabel(),paintLike(),toggleReplyLike(),ownsRow(); the handler is delegated on the Feed root, so a reply that lands later gets the control with no wiring and only that one node repaints.sendReply()now emits the like node inline whenownsRow(row). - No new state on the Feed state machine — this is behavior inside
active/win/just-joined-tour. data-repo:src/features/challenges/components/group-feed/group-feed.tsx→ becomes<GroupFeed state={…}/>.
- FeedCard — new state
try-granted(components/FeedCard/FeedCard.{html,js,css}, commitd657224) — the try-reset request and its resolution are one event read twice (TryRequest.statuspending → granted), so granting advances this card in place and never posts a second "gifted" item.- Declared as a real transition on the state machine (
'try-request': { on: { gift: 'try-granted' } }), not a CSS class flip — "granted" is a state of the event, not a decoration on the request. The old.is-grantedclass flip is removed. - The granted face goes mint, drops the out-of-tries desaturation on the game tile (
filter:none), swaps the per-state copy (Out of tries · needs a reset→Reset gifted · is back in) and names + shows the crewmate who stepped in, or "You" when you granted it yourself (instance._giftedByMe). .request-card__grantedkeepsmin-height:34pxso resolving a request never jolts the stream.- Neither state carries reactions or a Reply — granting IS the response.
- Component count is now twelve states, five faces.
data-repo:src/features/challenges/components/group-feed/feed-item.tsx→ becomes<FeedItem state={…}/>; subsumes the repo'scelebration-card.tsx/system-card.tsx/group-win-card.tsxtrio.
- Declared as a real transition on the state machine (
- Your Updates (
components/YourUpdates/YourUpdates.css, commit7244196) — drawer headalign-items: flex-start→center, so the close X centres against the whole title block instead of riding high next to a two-line head. Visual only; no state, model or binding change.
Data model — the fields to bind
Reply.likedByOwner: boolean(new) — replaces any notion of alikers[]list. One bit, because a reply can only ever have one liker. NolikeCount— the bit is the count. Also added:Reply.replyId(the mutation's handle).- The name in the read-only form is the EVENT's author (
item.author), not a reply field — one name, never a list, so the label can never outgrow the 322px feed column. TryRequest.grantedBy: { name, avatarSlug } | null(new) — null until the request resolves; the only field the granted face adds.TryRequest.status('pending' | 'granted') is the state key fortry-requestvstry-granted.
Contracts (HAND-OFF NOTE DATA / ACTIONS)
toggle-reply-like→likeFeedReply(replyId, isActive)(proposed mutation) — flips the reply'slikedByOwner; the label re-reads itself from the new payload, so there is no second string to keep in sync. Optimistic, one node wide — copy the shape ofsrc/features/challenges/lib/feed-reactions.ts(toggleSelfReaction).- Server-side authorisation is required: the mutation must be rejected from anyone but the event's owner. The client rule is the affordance, not the guard.
gift-reset→giftTryReset(requestId)(proposed, updated) — premium-only; advances THIS event totry-granted(status → 'granted',grantedBy → the caller). The mutation must return the updatedFeedEventso the card re-renders from server truth, not from the click, and the resolved card must come back on the same event id — never as a new feed item.- Feed
DATAgap list updated: the replies edge now oweslikedByOwner+likeFeedReplyalongsideevent.replies[]+postReply.
Motion
- Like landing pop (
components/Feed/Feed.css) —feed-like-in, 260mscubic-bezier(0.32,0.72,0,1),scale(.78) → 1.12 → 1on the like node when it is given. Plus:active { transform: scale(.94) }press feedback and a 120ms colour transition. transform/opacity only, and gated: underprefers-reduced-motionthe animation and the press scale are dropped and only the colour transition survives — the state still arrives in full, because the like is a colour + word change and the pop is the only motion. - FM equivalent:
<motion.button animate={{scale:[.78,1.12,1]}} transition={{duration:.26, ease:[0.32,0.72,0,1]}}/>,whileTap={{scale:.94}}; see the porting table inREADME.md.
Assets
assets/icons/lucide/thumbs-up.svg— lucide-static v1.27.0, line/outline only, used as a CSS mask (.feed-reply__like-i) so it recolours with the label from a file. One mark in every state; the colour carries the state. Carriesxmlns(required for a standalone mask file). Theurl()lives inFeed.cssand is root-absolute.
Registration
No LAYOUT or CATALOG changes needed — challenges/gallery.js catalogues feedCardDef with no states: filter, so the new try-granted state is covered automatically. check.py PASSes.
Open questions for the dev
likeFeedReplyauthorisation is stated but not designed here: confirm the server derives the event owner from the event, not from a client-sent field.- The read-only attribution renders the event author's display name. Confirm the feed payload already carries it on every person-directed event, or the granted/liked line will need it added.