Skip to content

Blocking & safety

How Objectuve lets you block someone who's crossed a line, what a block actually hides, and how reporting fits alongside it. Shipped in the v4.23 "Guideline 1.2 User Blocking" milestone (PRs #1873, #1883, #1884) — this closes the App Store Guideline 1.2 gap where blocking used to be ally-scoped only.

Overview

Block is a first-class safety action, not an ally-management setting. You can block anyone whose content you encounter, right from where you encounter it — you don't need an existing ally relationship, and blocking doesn't require going through Settings first. A block is symmetric: once it's in place, neither of you sees the other's content anywhere in the app. It's also fully reversible — unblock any time from Settings, no waiting period, no admin involved.

Report is a separate action for flagging content to Objectuve's moderation team. Today it's wired on community posts; see Report today vs. tomorrow for the exact boundary.

Where you can block someone

Block is available from four surfaces — anywhere you encounter another member's content or profile:

SurfaceComponentEntry point
Community postCommunityFeedCard.vuePost overflow menu — "Block {name}", below Report/Mute with a divider
Post commentCommunityFeedCard.vueComment kebab → action sheet
Goal-event commentGoalEventCard.vueComment kebab → action sheet
Public profilePublicProfile.vueKebab beside Share, in the profile hero

Every surface routes through the same shared component, BlockConfirmSheet.vue, which fires BLOCK_ALLY_MUTATION (blockAlly(targetUserId), backed by Social::BlockAlly). The confirm sheet's copy, verbatim:

Block {name}? You'll stop seeing each other everywhere — posts, comments, goals, and search. {name} won't be notified. You can undo this anytime in Settings.

On success, a toast confirms the block and offers an Undo action for a few seconds (unblockUser, same as a manual unblock).

What a block hides

Blocking suppresses content in both directions — this isn't a mute or a one-way filter. Social::BlockAlly reuses the existing UserAlly model: it either flips an existing pending/accepted row to status: 'blocked', or creates a new blocked row if the two of you had no relationship at all. No schema migration was needed to ship this.

UserAlly.blocked_user_ids_for(user) (user_ally.rb) computes the caller's full blocked set from either column (user_id or ally_id) — so it doesn't matter who blocked whom, both people disappear from each other's content. Every read path that can surface another user's content excludes this set:

  • Community feed, community members, community goals (community_queries.rb)
  • The unified activity feed, including notification-item actors (build_unified_feed.rb)
  • The ally activity feed (build_ally_activity_feed.rb)
  • Nested comments, reactions, and encouragements on goals and goal events (goal_type.rb, goal_event_type.rb)
  • Community post likes and comments (community_post_type.rb)
  • searchUsers — a blocked user never turns up in search, in either direction
  • Your ally list and any activity that would otherwise flow through it

All of this is centralized in BlockingHelpers, which memoizes the blocked-id set once per GraphQL request regardless of how many resolvers/types consult it.

Unblocking

Manage your blocks from Settings → Privacy → Blocked (/settings/blocked, BlockedUsersPage.vue). The list shows each blocked person's name, avatar, and handle — with no relative timestamp (see Known gaps below) — and an Unblock button per row, routed through the same BlockConfirmSheet (unblock variant). Confirm copy, verbatim:

Unblock {name}? You'll both be able to find and message each other again. This won't restore your old ally connection.

Unblocking calls unblockUser(targetUserId), backed by Social::UnblockAlly, which destroys the UserAlly row outright rather than reverting it to a prior status. Practically: if you were allies before one of you blocked the other, unblocking does not bring the ally connection back — you're both returned to "no relationship," and would need to send a fresh ally request to reconnect.

Report today vs. tomorrow

Report and Block are two different actions with two different current footprints, and they don't yet fully match:

  • Report is wired in the app UI on community posts only — the overflow menu on a community post opens ReportContentModal.vue (reasons: spam, harassment, profanity, discouraging, other), which files a ContentReport for the moderation team to review (see Admin Dashboard Guide § Content Reports for that side).
  • The backend already accepts reports on post comments and goal-event commentsContentModeration::ReportContent::ALLOWED_TYPES is %w[CommunityPost PostComment GoalEventComment] — but no frontend affordance calls it for those two types yet. There's no reportable type for a user profile at all.
  • Block, by contrast, ships on all four surfaces today — post, post comment, goal-event comment, and public profile — with no backend gap.

So Block has broader coverage than Report right now, not the other way around. Wiring a Report entry point onto comments and profiles (the backend already supports two of the three) is a scoped follow-up, not part of this milestone — see the v4.23 Phase 2 UI-SPEC's "Report parity across surfaces" note for the original call.

GraphQL API

Mutations

  • blockAlly(targetUserId: ID!): { success, errors } — creates or updates a UserAlly to status: 'blocked'. Guards: must be authenticated, can't block yourself, target must exist.
  • unblockUser(targetUserId: ID!): { success, errors } — destroys the blocked UserAlly row. Guards: must be authenticated, can't unblock yourself, target must exist, a block must actually exist between you.

Queries

  • blockedUsers: [UserSearchResult!]! — everyone you've blocked or who has blocked you (symmetric, same as the content-filtering set), each with ally_status: 'BLOCKED'. Exposes publicId, firstName, lastName, username, photo — no blocked_at (see below).

Known gaps

  • No "blocked N days ago" timestamp. blockedUsers doesn't expose a blocked_at field, so the Settings list shows just the person's handle — no relative time. Adding one is a small, flagged, post-milestone candidate (backend + frontend), not something currently shipped.
  • Report parity across the four surfaces — see above.

See also


Version: v4.3.2 (latest tag — this feature is unreleased pending the next production tag; see ionic_frontend/CHANGELOG.md [Unreleased]) Last updated: 2026-07-29

Loading…