Skip to content

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/app has no add_goal_encouragement mutation 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:

  1. From a goal timeline (detail page or public goal page), find an event card
  2. Tap the heart icon on the event card
  3. The encouragement toggles (add if not yet encouraged, remove if already)
  4. 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: encouragement notification 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:

  1. From a goal timeline, expand an event card
  2. Tap the comment icon or section
  3. Type a comment in the input field
  4. Submit the comment
  5. Comment appears below the event with author name and timestamp

What Gets Stored:

  • content: Comment text (required)
  • goal_event_id: FK to the event
  • user_id: Auto-set from authenticated user
  • created_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: comment notification to the goal owner

Public Goals

Making a Goal Public

  1. Navigate to a goal's detail page
  2. Tap the FAB menu → Toggle Privacy
  3. Goal switches from private to public (or vice versa)
  4. 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

  1. Navigate to a public goal page
  2. Tap the Follow button
  3. The goal appears in your activity feed
  4. You receive notifications when the goal owner posts updates

How to Unfollow

  1. Navigate to the followed public goal
  2. 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:

TriggerNotification KindContent
Someone encourages your goalencouragement"{name} encouraged your goal"
Someone comments on your updatecomment"{name} commented on your update"
Someone follows your goalfollow"{name} is following your goal"
Goal reminder (past due)past_due"Your goal needs attention"
Badge earnedbadge"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

bash
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.rb

Frontend Unit Tests

bash
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

graphql
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

graphql
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

graphql
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

  1. SocialNudgeCard displays a community encouragement notification
  2. If the notification has a sender_user_public_id, show Send 💪 button; otherwise show passive Nice!
  3. Tap "Send 💪" to send a reciprocal encouragement to the original sender
  4. Confirmation state displays "Encouragement sent!" for ~600ms before advancing to the next card
  5. 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, uses acts_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: GoalEventReactionType with id, emoji, user, createdAtTime
  • reactions field added to GoalEventType and fetched in all goal event queries

Community Post Comments

Community feed posts support inline comments:

  • Backend: AddPostComment interaction + addPostComment GraphQL 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 goalId navigate to the public goal page on comment click

Known Behaviors & Edge Cases

  1. 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.

  2. Public goal access control: The PUBLIC_GOAL_QUERY only returns goals where private: false. Attempting to access a private goal's public URL returns null.

  3. Dynamic meta tags: The useDynamicMeta() composable sets og:title, og:description, and og:image for social sharing previews. These update on route change.

  4. 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.

  5. Follow notifications: Social::ToggleFollowGoal fires a notification and feed entry on every new follow (and on restoring a previously-unfollowed follow) via add_follow_notification_and_feed (UserNotification.add_follow_notification + UserFeedItem.add_follow_feed_item). This matches the follow row in the Notifications table below — the goal owner is notified, it is not silent.

  6. Comment ordering: Comments appear in chronological order (oldest first) under each event. There is no pagination — all comments load at once.

  7. Encouragement avatars: The goal detail page shows up to N encouragement avatars in a grid. Additional encouragers are shown as "+X more".

  8. Non-authenticated sharing: Users can view public goals without signing in. The "Sign in to encourage" CTA redirects to /sign-in with a return URL to come back to the public goal after authentication.

Last updated: 2026-07-06

Loading…