Social Features — Feature & Testing Guide
Overview
Objectuve's social layer enables accountability through encouragements (likes), comments on goal updates, public goal sharing, goal following, and an ally activity feed. These features create a lightweight social network focused on supporting each other's goals rather than endless browsing — matching the Duolingo-style "quick check-in" philosophy.
Related guides: Allies (ally request system and activity feed) · Notifications (encouragement, comment, and follow notification delivery) · Activity Feed (unified feed aggregation)
Encouragements
Goal Encouragements (deprecated, not wired)
ADD_GOAL_ENCOURAGEMENT_MUTATION is defined in the frontend GraphQL constants (ionic_frontend/src/constants/graphql/social.js), but it is dead code:
- No component or composable imports or calls
ADD_GOAL_ENCOURAGEMENT_MUTATION— grepping the frontend for its usage returns zero hits outside the constant definition. - There is no server-side registration:
rails_api/apphas noadd_goal_encouragementmutation class, resolver, or interaction. Calling this mutation would fail against the live schema.
This is the same legacy interaction referenced in Known Behaviors #1 below — the walkthrough that used to exist here (heart button on a public goal → GoalEncouragement record → notification) is superseded by Event Card encouragements and Emoji Reactions.
Goal Event Encouragements
Users can encourage individual progress updates (events) on any goal they can see.
How it works:
- From a goal timeline (detail page or public goal page), find an event card
- Tap the heart icon on the event card
- The encouragement toggles (add if not yet encouraged, remove if already)
- Count updates immediately
GraphQL: TOGGLE_GOAL_EVENT_ENCOURAGEMENT_MUTATION with param goalEventId
Key Behaviors
- Goal-level encouragements (
AddGoalEncouragement) are dead code — see above; nothing calls them today - Event encouragements are toggleable — clicking again removes the encouragement
- Encouragements trigger a
kind: encouragementnotification to the goal owner - Encouragement avatars are displayed as a grid on the goal detail page
Comments
Goal Event Comments
Users can comment on goal progress updates.
How it works:
- From a goal timeline, expand an event card
- Tap the comment icon or section
- Type a comment in the input field
- Submit the comment
- Comment appears below the event with author name and timestamp
What Gets Stored:
content: Comment text (required)goal_event_id: FK to the eventuser_id: Auto-set from authenticated usercreated_at: Timestamp
GraphQL: ADD_GOAL_EVENT_COMMENT_MUTATION with params goalEventId and content
Key Behaviors
- Comments are append-only — no edit or delete in the current UI
- Each comment shows: author name, avatar, content, and relative timestamp
- Comment count is displayed on event cards in the timeline
- Comments trigger a
kind: commentnotification to the goal owner
Public Goals
Making a Goal Public
- Navigate to a goal's detail page
- Tap the FAB menu → Toggle Privacy
- Goal switches from private to public (or vice versa)
- Public goals get a Public badge in the header
Public Goal Page
Public goals are accessible at /public_goal/:goalId without authentication.
What's visible to everyone:
- Goal name, description, category, cover image
- Owner name, avatar, and start date
- Progress percentage and progress bar
- Timeline of events (content, images, timestamps)
- Encouragement count
- Milestone progress stats
What's NOT visible:
- Private goals cannot be accessed (404)
- No editing or management controls for viewers
Sharing
- Tap the Share button on the public goal page
- Uses
navigator.share()(native share sheet) if available - Falls back to copying the URL to clipboard
- Dynamic meta tags (title, description, image) are set via
useDynamicMeta()for social sharing previews
Following Public Goals
How to Follow
- Navigate to a public goal page
- Tap the Follow button
- The goal appears in your activity feed
- You receive notifications when the goal owner posts updates
How to Unfollow
- Navigate to the followed public goal
- Tap the Following button to unfollow
GraphQL: TOGGLE_FOLLOW_GOAL_MUTATION with param goalId
Check follow status: IS_FOLLOWING_PUBLIC_GOAL_QUERY with param id
Ally Activity Feed
What It Is
The ally activity feed shows recent progress from your accepted ally connections (friends). It appears on the Dashboard as a section showing the latest activity from people in your network.
Feed Content
Each feed item shows:
- Ally name and avatar
- Action description (e.g., "completed a goal", "checked in")
- Target name (goal or community)
- Timestamp
- Optional community context
GraphQL: ALLY_ACTIVITY_FEED_QUERY with params userId and limit
Display
- Dashboard shows up to 3 ally feed items
- "See more" link expands to full feed
- Empty state if no allies or no recent activity
Notifications
Social interactions generate notifications delivered in real-time via WebSocket:
| Trigger | Notification Kind | Content |
|---|---|---|
| Someone encourages your goal | encouragement | "{name} encouraged your goal" |
| Someone comments on your update | comment | "{name} commented on your update" |
| Someone follows your goal | follow | "{name} is following your goal" |
| Goal reminder (past due) | past_due | "Your goal needs attention" |
| Badge earned | badge | "You unlocked {badge name}!" |
Notification Panel
- Accessed via the bell icon in the app header
- Shows unread count badge (red)
- List of notifications sorted by recency
- Each notification: icon, content text, timestamp
- Tap to navigate to the related entity
- Acknowledge individual notifications or all at once
Subscriptions: NOTIFICATION_UPDATE_SUBSCRIPTION for real-time delivery
UI Components
Public Goal Page (PublicGoal.vue)
- Sticky header with Public badge and share button
- Hero section with cover image and gradient overlay
- Owner card with avatar, name, start date, encourage button
- Progress card with percentage bar
- Timeline of events with encourage/comment interactions
- Sticky CTA footer for non-logged-in users: "Sign in to encourage"
NotificationsPanel
- Bell icon with unread count badge
- Dropdown/slide panel with notification list
- Filter by type
- Acknowledge all button
- Individual dismiss actions
Event Card (in Timeline)
- Content text, optional image
- Heart icon with encouragement count
- Comment icon with comment count
- Expandable comment section
- Author name and timestamp
Testing the Feature
Manual Testing Checklist
1. Encourage a Public Goal
- [ ] Create a goal and make it public (toggle privacy)
- [ ] Copy the public goal URL
- [ ] Sign in as a different user (or use incognito + sign in)
- [ ] Navigate to the public goal URL
- [ ] Tap the heart button
- [ ] Verify encouragement count increments
- [ ] Switch to the goal owner account → verify notification received
2. Encourage a Goal Event
- [ ] Navigate to any goal with timeline events
- [ ] Tap the heart icon on a specific event
- [ ] Verify count increments and icon fills
- [ ] Tap again → verify encouragement is toggled off and count decrements
3. Comment on a Goal Event
- [ ] Navigate to a goal with timeline events
- [ ] Expand the comment section on an event
- [ ] Type a comment and submit
- [ ] Verify comment appears with your name and timestamp
- [ ] Switch to goal owner → verify comment notification received
4. Make a Goal Public
- [ ] Navigate to a private goal's detail page
- [ ] Tap FAB → Toggle Privacy
- [ ] Verify Public badge appears in the header
- [ ] Copy the URL and open in incognito → verify accessible
- [ ] Toggle back to private → verify incognito access returns 404
5. Share a Public Goal
- [ ] Navigate to a public goal
- [ ] Tap the Share button
- [ ] On mobile: verify native share sheet opens
- [ ] On desktop: verify URL is copied to clipboard
- [ ] Paste the shared URL → verify it loads the public goal
6. Follow/Unfollow a Public Goal
- [ ] Navigate to a public goal (not your own)
- [ ] Tap Follow → verify button changes to "Following"
- [ ] Have the goal owner post an update → verify you receive a notification
- [ ] Tap Following to unfollow → verify button reverts
- [ ] Verify you no longer receive notifications for this goal
7. Ally Activity Feed
- [ ] On Dashboard, verify Ally Activity section appears (if you have allies)
- [ ] Verify feed items show ally name, action, goal, timestamp
- [ ] Verify "See more" link works
- [ ] If no allies: verify empty state or section is hidden
8. Notifications
- [ ] Trigger a social action (encourage, comment, follow)
- [ ] Verify notification bell badge increments
- [ ] Open notification panel → verify new notification appears
- [ ] Tap notification → verify navigation to related entity
- [ ] Acknowledge notification → verify count decrements
- [ ] Tap "Acknowledge All" → verify all marked as read
9. Non-authenticated Public Goal
- [ ] Open a public goal URL in incognito (not signed in)
- [ ] Verify goal details are visible
- [ ] Verify "Sign in to encourage" CTA appears at bottom
- [ ] Verify encourage/comment buttons are not accessible
- [ ] Tap sign-in CTA → verify redirect to sign-in page
Backend Unit Tests
cd rails_api
# Interaction specs
bundle exec rspec spec/interactions/social/toggle_goal_event_encouragement_spec.rb
bundle exec rspec spec/interactions/social/add_goal_event_comment_spec.rb
bundle exec rspec spec/interactions/social/toggle_follow_goal_spec.rb
# GraphQL specs
bundle exec rspec spec/requests/mutations/toggle_goal_event_encouragement_spec.rb
bundle exec rspec spec/requests/mutations/add_goal_event_comment_spec.rb
bundle exec rspec spec/requests/mutations/toggle_follow_goal_spec.rbFrontend Unit Tests
cd ionic_frontend
npm run test:unit -- --run -t "PublicGoal"
npm run test:unit -- --run -t "NotificationsPanel"
npm run test:unit -- --run -t "useNotifications"GraphQL API Reference
Queries
query PublicGoalQuery($id: ID!) {
publicGoal(id: $id) {
publicId
name
content
targetDateTime
completed
completedAtTime
imageUrl
kind { id name }
category { id name }
allEvents {
publicId content createdAtTime
media { imageUrl }
encouragements { id user { publicId firstName lastName } }
comments { id content createdAtTime user { publicId firstName lastName } }
}
milestones { publicId name completed completedAtTime }
user { publicId firstName lastName username photo { imageUrl } }
}
}
query IsFollowingGoal($id: ID!) {
isFollowingGoal(id: $id) { success }
}
query AllyActivityFeed($userId: ID!, $limit: Int) {
allyActivityFeed(userId: $userId, limit: $limit) {
id allyId allyName allyPhoto action target
communityId communityName timestamp
}
}Mutations
mutation AddGoalEncouragement($goalId: ID!, $comment: String) {
addGoalEncouragement(goalId: $goalId, comment: $comment) {
encouragement { id comment goal { publicId } }
}
}
mutation ToggleGoalEventEncouragement($goalEventId: ID!) {
toggleGoalEventEncouragement(goalEventId: $goalEventId) {
goalEventEncouragement { id user { publicId } }
}
}
mutation AddGoalEventComment($goalEventId: ID!, $content: String!) {
addGoalEventComment(goalEventId: $goalEventId, content: $content) {
goalEventComment { id content createdAtTime user { publicId firstName lastName } }
}
}
mutation ToggleFollowGoal($goalId: ID!) {
toggleFollowGoal(goalId: $goalId) { result { success } }
}
mutation AcknowledgeNotification($id: ID!) {
acknowledgeNotification(id: $id) { result { success } }
}Subscriptions
subscription NotificationUpdate($userId: ID!) {
notificationUpdate(userId: $userId) {
id kind content detailsJson acknowledged createdAtTime
}
}Focus Mode — Reciprocal Encouragement
In Focus Mode, the SocialNudgeCard surfaces unacknowledged encouragements from community members. When an encouragement carries a sender's ID, users can now respond with a reciprocal "Send 💪" to acknowledge their supporter in the same flow.
How it works
- SocialNudgeCard displays a community encouragement notification
- If the notification has a
sender_user_public_id, show Send 💪 button; otherwise show passive Nice! - Tap "Send 💪" to send a reciprocal encouragement to the original sender
- Confirmation state displays "Encouragement sent!" for ~600ms before advancing to the next card
- Aggregated notifications (multi-sender) fall back to "Nice!" (no sender to respond to)
Rate limiting
- One reciprocal encouragement per sender→receiver per day (wall-clock day, UTC)
- Rate-limit errors are surfaced to the user via toast: "You have already sent an encouragement to this user today"
- Applies only to peer encouragements; goal-event encouragements don't count toward the limit
Error handling
- Network failures and server errors show a toast: "Could not send encouragement. Try again later."
- The card still advances regardless of success or failure, preserving focus-mode flow
GraphQL
Mutation: sendEncouragement(toUserPublicId: ID!)
Emoji Reactions
Emoji reactions are the primary social interaction on goal events, replacing the older binary encouragement toggle.
Model: GoalEventReaction
- Inherits
PublicRecord, usesacts_as_paranoid - One reaction per user per event (partial unique index on
(goal_event_id, user_id) WHERE deleted_at IS NULL) - 16 allowed emojis: 🚀 💪 🎯 🏁 🥇 🌟 ✨ 💥 🧨 👀 🙌 🤝 🐐 🛠️ 🔁 🧱
- 🔁 ties to "Visualize, Actualize, Repeat" theme; 🧱 ties to "stacking bricks" theme
Interaction: ToggleGoalEventReaction
- No existing reaction → creates new reaction, sends notification + feed item
- Same emoji → soft-deletes (toggle off)
- Different emoji → updates in place (no extra notification)
- Soft-deleted reaction → restores with new emoji
- Blocks reactions on private goals
Frontend: ReactionPicker component
- Summary row shows top 3 emojis with counts and total
- Tap opens horizontal scrolling picker tray with all 16 emojis
- User's current reaction highlighted with ring
- Integrated into
GoalEventCard,TimelineEvent,PublicTimelineEvent,GoalEventItem
GraphQL
- Mutation:
toggleGoalEventReaction(goalEventId: ID!, emoji: String!) - Type:
GoalEventReactionTypewithid,emoji,user,createdAtTime reactionsfield added toGoalEventTypeand fetched in all goal event queries
Community Post Comments
Community feed posts support inline comments:
- Backend:
AddPostCommentinteraction +addPostCommentGraphQL mutation - Frontend: Comment button toggles inline comments section with comment input
- Optimistic insertion: comments appear instantly, replaced by server response on success
- Posts with a
goalIdnavigate to the public goal page on comment click
Known Behaviors & Edge Cases
Goal vs event encouragements: Goal-level encouragements (AddGoalEncouragement) are legacy and no longer called from the frontend. Event-level encouragements (ToggleGoalEventEncouragement) are still functional but supplemented by emoji reactions (ToggleGoalEventReaction) which offer a richer interaction.
Public goal access control: The
PUBLIC_GOAL_QUERYonly returns goals whereprivate: false. Attempting to access a private goal's public URL returns null.Dynamic meta tags: The
useDynamicMeta()composable setsog:title,og:description, andog:imagefor social sharing previews. These update on route change.WebSocket notifications: Notifications are delivered via ActionCable subscription. If the WebSocket connection drops, notifications will appear on next page load via the regular
USER_QUERY.Follow notifications:
Social::ToggleFollowGoalfires a notification and feed entry on every new follow (and on restoring a previously-unfollowed follow) viaadd_follow_notification_and_feed(UserNotification.add_follow_notification+UserFeedItem.add_follow_feed_item). This matches thefollowrow in the Notifications table below — the goal owner is notified, it is not silent.Comment ordering: Comments appear in chronological order (oldest first) under each event. There is no pagination — all comments load at once.
Encouragement avatars: The goal detail page shows up to N encouragement avatars in a grid. Additional encouragers are shown as "+X more".
Non-authenticated sharing: Users can view public goals without signing in. The "Sign in to encourage" CTA redirects to
/sign-inwith a return URL to come back to the public goal after authentication.
Last updated: 2026-07-06