## Phase 1: Critical Feature Completion (Beacon Voting) - Add VouchBeacon, ReportBeacon, RemoveBeaconVote methods to PostRepository - Implement beacon voting HTTP handlers with confidence score calculations - Register new beacon routes: /beacons/:id/vouch, /beacons/:id/report, /beacons/:id/vouch (DELETE) - Auto-flag beacons at 5+ reports, confidence scoring (0.5 base + 0.1 per vouch) ## Phase 2: Feed Logic & Post Distribution Integrity - Verify unified feed logic supports all content types (Standard, Quips, Beacons) - Ensure proper distribution: Profile Feed + Main/Home Feed for followers - Beacon Map integration for location-based content - Video content filtering for Quips feed ## Phase 3: The Notification System - Create comprehensive NotificationService with FCM integration - Add CreateNotification method to NotificationRepository - Implement smart deep linking: beacon_map, quip_feed, main_feed - Trigger notifications for beacon interactions and cross-post comments - Push notification logic with proper content type detection ## Phase 4: The Great Supabase Purge - Delete function_proxy.go and remove /functions/:name route - Remove SupabaseURL, SupabaseKey from config.go - Remove SupabaseID field from User model - Clean all Supabase imports and dependencies - Sanitize codebase of legacy Supabase references ## Phase 5: Flutter Frontend Integration - Implement vouchBeacon(), reportBeacon(), removeBeaconVote() in ApiService - Replace TODO delay in video_comments_sheet.dart with actual publishComment call - Fix compilation errors (named parameters, orphaned child properties) - Complete frontend integration with Go API endpoints ## Additional Improvements - Fix compilation errors in threaded_comment_widget.dart (orphaned child property) - Update video_comments_sheet.dart to use proper named parameters - Comprehensive error handling and validation - Production-ready notification system with deep linking ## Migration Status: 100% Complete - Backend: Fully migrated from Supabase to custom Go/Gin API - Frontend: Integrated with new Go endpoints - Notifications: Complete FCM integration with smart routing - Database: Clean of all Supabase dependencies - Features: All functionality preserved and enhanced Ready for VPS deployment and production testing!
19 KiB
Sojorn Calm UX Guide
Design Philosophy
Sojorn enforces calm through intentional friction, respectful boundaries, and visual restraint. Every interaction should feel like settling in, not being rushed.
Part 1: Reading & Feed Experience
Design Intent
- Reading feels like settling into a book, not scrolling a timeline
- No visual shouting or metric obsession
- No urgency cues or FOMO mechanics
Feed Layout Decisions
1. Comfortable Max Width (680px)
Why:
- Optimal line length for reading is 50-75 characters
- Wide text blocks strain eye tracking
- Creates "settling in" feeling vs infinite scroll anxiety
Implementation:
Container(
constraints: const BoxConstraints(maxWidth: 680),
// ...
)
2. Generous Vertical Spacing (24px between posts)
Why:
- Tight spacing = rushed feeling
- Space = permission to pause
- Each post gets breathing room
Visual Effect:
- Posts feel like chapters in a book
- No "wall of content" anxiety
3. No Aggressive Dividers
Why:
- Heavy dividers create visual noise
- Soft shadows and spacing create natural separation
- Avoids the "list item" feeling
Implementation:
boxShadow: AppTheme.shadowSm // Only 4% opacity
Post Card Design Decisions
1. Body Text is THE HERO
Why:
- You came here to read, not to scan metrics
- Large, comfortable type (17px at 1.7 line-height)
- Primary visual weight goes to content
Implementation:
Text(
post.body,
style: AppTheme.bodyLarge.copyWith(
height: 1.7, // Extra generous for reading
letterSpacing: 0.1, // Slight tracking
),
)
Contrast with Twitter/X:
- Twitter: 15px text, 1.4 line-height, competing with images/metrics
- Sojorn: 17px text, 1.7 line-height, nothing competes
2. Author Identity: Clear but Not Dominant
Why:
- You need to know who's speaking
- But not at the expense of the message
- Small avatar (36px vs typical 48px)
- De-emphasized text color (textSecondary, not textPrimary)
Visual Hierarchy:
1. Post body (textPrimary, 17px, bold visual weight)
2. Actions (tertiary, 16px icons)
3. Author (textSecondary, 13px)
4. Metadata (textTertiary, 11px)
3. Metrics De-emphasized
Why:
- Like counts don't mean quality
- Save counts are personal, not performance
- Comment counts ≠ value (and don't boost reach!)
Design Choices:
- Icons are small (18px, not 24px)
- Text is labelSmall (11px)
- Color is textTertiary (very light gray)
- No visual weight
What's Hidden:
- View counts (never shown)
- Ratio metrics (likes/comments)
- Trending indicators
4. Trust Tier Badge: Subtle Signal
Why:
- Trust matters for context
- But shouldn't dominate
- Tiny badge (8px font, 12% opacity background)
vs Other Platforms:
- Twitter verification: 20px, bright blue, dominant
- Sojorn trust tier: 8px, muted color, barely visible
Interaction Behavior
1. Gentle Press States
Why:
- Aggressive hover/press = visual aggression
- Subtle border change (borderSubtle → borderStrong)
- Shadow removal (not addition)
Implementation:
border: Border.all(
color: _isPressed ? AppTheme.borderStrong : AppTheme.borderSubtle,
width: 0.5,
),
boxShadow: _isPressed ? null : AppTheme.shadowSm,
Effect:
- Card "settles in" when pressed
- No bounce, no scale, no aggressive feedback
2. No Metric Celebration
Why:
- No confetti when you hit 10 likes
- No "trending" badges
- No "your post is doing well!" notifications
Philosophy:
- You wrote something calm → reward is internal
- External validation ≠ quality
Reading Enhancements
1. Optimal Typography
- 17px body text (larger than most platforms)
- 1.7 line-height (research shows 1.5-1.6 is ideal, we go further)
- 0.1 letter-spacing (slight tracking for comfort)
- System fonts (SF Pro Text, Roboto) for familiarity
2. Interaction Affordances
- Appreciate/Save always visible but quiet
- No hidden menus (everything upfront)
- Tap target size: 44px minimum (accessibility)
Part 2: Writing & Commenting Experience
Composer Design Intent
- Writing pauses before publishing (no tweet-and-regret)
- Friction feels supportive, not punitive
- Tone guidance is optional and respectful
Composer UX Decisions
1. Large, Calm Text Area
Why:
- Small inputs = rushed thoughts
- Large area = room to think
- 500 character limit shown gently (not alarming)
Design:
SojornTextArea(
minLines: 5, // Not 1-2 like Twitter
maxLines: 15,
maxLength: 500,
style: AppTheme.bodyLarge, // Same size as reading
)
2. Character Limit: Gentle, Not Alarming
What We Don't Do:
- ❌ Turn red at 480/500
- ❌ Show "You're over the limit!" in red text
- ❌ Disable publish button aggressively
What We Do:
- ✅ Show "487 / 500" in textTertiary
- ✅ Turn accent color at 490
- ✅ Fade publish button (not disable) at 501+
Copy:
Good: "487 / 500"
Bad: "ONLY 13 CHARACTERS LEFT!"
3. Category Selection: Clear and Required
Why:
- Categories are structural boundaries
- Must be intentional (no "general" default)
- Clear labels, obvious UI
Tone Nudge UI
1. When Triggered
Scenario: Post gets CIS < 0.85
What We Don't Do:
- ❌ Red warning banner
- ❌ "This violates community guidelines"
- ❌ Block publishing immediately
- ❌ Shame the user
What We Do:
- ✅ Neutral language
- ✅ Soft amber background (not red)
- ✅ Suggestion, not demand
- ✅ Allow dismiss
Copy:
## Sharp Edges Detected
This post has language that may feel sharp to readers.
**Suggested rewrite:**
"I respectfully disagree with this approach."
**Original:**
"This is stupid and wrong."
[ Publish Anyway ] [ Edit ]
Tone:
- No "You violated..."
- No "This is not allowed"
- Just "This may feel sharp"
2. Allow Dismiss Without Penalty
Why:
- You're an adult
- Tone detection isn't perfect
- Trust users to make decisions
But:
- Persistent low CIS → harmony score impact
- 3+ rejected posts → temporary slow-down
- Trust tier may adjust
Philosophy:
- Friction, not force
- Consequences, not punishment
Comment UI
1. Mutual-Follow Only
Design:
- Comment box only appears if mutual follow
- Otherwise: "Follow each other to comment"
- No shame, just structure
2. Compact, Conversational Layout
Why:
- Comments are dialogue, not performance
- Small avatars (28px)
- Lighter visual weight than posts
3. Downvotes: De-emphasized
Why:
- Downvotes useful for spam/quality
- But not a weapon
Design:
- No downvote count shown
- Icon is tertiary gray (not red)
- No "controversial" indicators
Empty States
1. No Pressure to Post
Bad Copy:
"Your feed is empty! Start following people!"
"Nothing to see here. Get active!"
Good Copy (Sojorn Voice):
"Nothing here yet"
"Posts you appreciate will appear here"
"Your feed is quiet right now"
Tone:
- Welcoming, not urgent
- Calm, not demanding
- Permission to lurk
Part 3: Navigation & Information Architecture
Design Intent
- Users always know where they are
- No hidden mechanics or dark patterns
- No surprise destinations
Bottom Navigation
1. Clear, Limited Tabs
What We Have:
- Following (chronological from follows)
- Sojorn (algorithmic FYP)
- Profile (your stats and posts)
What We Don't Have:
- ❌ "Discover" (too vague)
- ❌ "Notifications" (reduces checking anxiety)
- ❌ "Messages" (not yet implemented)
Why 3 Tabs:
- Cognitive load: 3-5 is ideal
- Each tab has one job
- No confusion
2. No Surprise Destinations
Rule:
- Tab icon = where you land
- No "Following but actually Explore"
- No "Profile but actually Settings"
Profile Hierarchy
1. Posts First
Why:
- You came to see what they wrote
- Not their bio or follower count
Layout:
1. Posts (primary view)
2. Bio (secondary, collapsed)
3. Stats (tertiary, small)
4. Controls (obvious but not dominant)
2. Follow/Unfollow: Obvious
Design:
- Always visible in header
- Clear label ("Follow" / "Following")
- No hidden in "..." menu
Settings Organization
1. Grouped by Concern
📖 Reading & Filters
- Category preferences
- Content filters
- Feed preferences
🔒 Privacy & Blocking
- Blocked users
- Profile visibility
- Data sharing
👤 Account & Data
- Email/password
- Export data
- Delete account
Why This Order:
- Most common (reading) first
- Safety (blocking) never buried
- Destructive (delete) last
2. No Buried Safety Controls
Rule:
- Block button on every profile
- Privacy settings in top-level menu
- Export data always accessible
Discoverability
1. Explore Tab Clearly Separate
Why:
- "Explore" ≠ "Following"
- No accidental algorithm exposure
- Opt-in discovery
2. Categories Never Auto-Enable
Rule:
- All categories off by default (except general)
- Explicit opt-in required
- No "We think you'd like..." suggestions
Part 4: Blocking & Filtering UX
Design Intent
- Blocking is self-care, not confrontation
- Filtering is private and encouraged
- No shame, no drama
Block Affordance
1. Always Visible
Where:
- On every profile (header menu)
- On every post (overflow menu)
- In comment threads
Design:
Icon: shield_outline (not block_circle)
Label: "Block @username"
Color: textSecondary (not error red)
Why Shield Icon:
- Block = protection, not punishment
- Shield = self-care
- Less aggressive than ⛔
2. One-Tap Confirmation
Flow:
Tap "Block" →
Dialog: "Block @username?"
"You won't see their posts or comments. They won't be notified."
[ Cancel ] [ Block ]
No:
- ❌ "Are you SURE?"
- ❌ "This is permanent"
- ❌ Multiple confirmations
Copy Tone:
- Neutral, not dramatic
- Reassuring, not scary
3. No Explanation Required
Why:
- You don't owe anyone an explanation
- Block is personal boundary
- No "Report" pressure
But:
- Separate "Report" option exists
- Report ≠ block (different flows)
Filter Controls
1. Category Toggles
Design:
[ ] Quiet Reflections
[ ] Gratitude
[x] General Discussion
[ ] Deep Questions
Each Shows:
- Name
- Description (one sentence)
- Post count (optional)
2. Keyword/Topic Filters (Optional)
Future Feature:
Hide posts containing:
- "election"
- "crypto"
- "diet"
Design:
- Off by default
- No suggestions
- No "trending" pressure
3. Preview What's Hidden (Optional)
Design:
[ ] Show me what I'm filtering
When Enabled:
- Filtered posts appear grayed out
- "Hidden by your filters" label
- Can tap to reveal
Default: OFF (out of sight, out of mind)
Feedback Copy
1. After Blocking
"You won't see posts from @username anymore."
"They won't be notified."
Not:
❌ "User blocked successfully!"
❌ "You'll never see them again!"
2. After Filtering
"Quiet Reflections hidden from your feeds."
Not:
❌ "Category disabled!"
❌ "You won't miss anything important"
Export/Import Block List
1. Clear Warnings
## Export Block List
This downloads a JSON file with usernames you've blocked.
⚠️ This file contains your personal blocking decisions.
⚠️ Sharing this may reveal your social boundaries.
⚠️ Sojorn does not endorse public block list sharing.
[ Cancel ] [ Download JSON ]
2. No Recommendations
What We Don't Do:
- ❌ "Import from popular block lists"
- ❌ "People like you also block..."
- ❌ "Suggested blocks based on your follows"
Why:
- Block lists = personal boundaries
- No crowd-sourcing judgment
- No guilt by association
Part 5: Transparency & Explanation
Design Intent
- Calm confidence through clarity
- No mystery, no jargon
- Trust through honesty
"How Reach Works" Page
Content Structure
# How Sojorn Ranking Works
Posts in your Sojorn feed are ranked by **calm velocity**—a measure of genuine appreciation over time.
## What Boosts Posts
- ❤️ Appreciations (likes)
- 🔖 Saves (strong signal)
- ⏱️ Time spent reading (dwell time)
- 🎯 High Content Integrity Score (CIS)
## What Slows Posts
- ⏰ Age (older posts fade naturally)
- 👎 Downvotes (quality filter)
- 🚩 Low CIS (tone issues)
## What Doesn't Matter
- 💬 Comment count (dialogue ≠ quality)
- 👥 Author's follower count
- 📊 Retweets/shares (we don't have those)
## Why This Way
Calm velocity rewards thoughtful content, not viral outrage. Posts earn reach through genuine appreciation, not reaction-baiting.
Design Choices
- Plain language (no "algorithm" jargon)
- Bullet points (scannable)
- Emojis (visual anchors, but not excessive)
- Honesty (explicitly state what doesn't matter)
"Rules & Tone" Page
Content Structure
# Community Tone Guidelines
Sojorn welcomes all ideas, but requires calm expression.
## Focus: Tone, Not Ideology
We don't police what you think. We ask how you express it.
### ✅ Allowed
- "I disagree with that approach."
- "This feels uncomfortable to me."
- "I see it differently."
### ⛔ Not Allowed
- "This is stupid."
- "You're an idiot."
- "What the hell were you thinking?"
## Why Tone Matters
Sharp language creates defensiveness. Calm language creates dialogue.
## How We Detect Tone
- Pattern-based analysis (not perfect)
- Content Integrity Score (CIS)
- Human review for edge cases
## What Happens
- CIS < 0.85: Gentle nudge to rephrase
- CIS < 0.70: Post blocked
- Persistent low CIS: Harmony score impact
Tone Choices
- No shame ("not allowed" not "violations")
- Examples (show, don't just tell)
- Honesty (admit imperfection)
Contextual Help
1. "Why am I seeing this?"
Trigger: Tap "..." on any post
Copy:
## Why This Post
This appeared in your Sojorn feed because:
- High calm velocity (287 appreciates, 45 saves)
- Category: General Discussion (you're subscribed)
- Posted 2 hours ago
2. "Why can't I comment?"
Scenario: Non-mutual follow
Copy:
## Commenting
You can comment when you both follow each other.
This protects against drive-by harassment and ensures dialogue, not performance.
Tone:
- No "You're not allowed"
- Just "This is how it works"
Part 6: Accessibility & Inclusivity
Text Accessibility
1. Scalable Font Sizes
Implementation:
Text(
post.body,
style: Theme.of(context).textTheme.bodyLarge,
// Respects user's OS text size settings
)
Why:
- Low vision users need large text
- Flutter automatically scales with OS settings
- No hardcoded font sizes
2. High Readability Contrast
Ratios:
- textPrimary on background: 12.5:1 (AAA)
- textSecondary on background: 7.2:1 (AA)
- textTertiary on background: 4.8:1 (AA large text)
Why:
- WCAG AAA for body text
- Still feels calm (not harsh black on white)
Interaction Accessibility
1. Keyboard Navigation (Web)
Requirements:
- Tab through all interactive elements
- Enter/Space activates buttons
- Escape closes modals
- Arrow keys in lists
Implementation:
Focus(
onKey: (node, event) {
// Handle keyboard events
},
child: Widget(),
)
2. Screen Reader Labels
Example:
IconButton(
icon: Icon(Icons.favorite_border),
tooltip: 'Appreciate this post',
semanticsLabel: 'Appreciate post from ${post.author.displayName}',
)
Why:
- Icon alone = meaningless to screen readers
- Context matters
3. Focus States
Design:
focusColor: AppTheme.accent.withValues(alpha: 0.1),
// Soft highlight, not harsh outline
Motion Accessibility
1. Respect Reduced Motion
Check:
final reducedMotion = MediaQuery.of(context).accessibleNavigation;
final duration = reducedMotion
? Duration.zero
: AppTheme.durationMedium;
Where Applied:
- Fade transitions
- Sheet slides
- Loading spinners
2. No Required Animations
Rule:
- All info accessible without animation
- Skeleton loaders have static alt
- Progress shown via text too
Cognitive Load
1. Avoid Dense UI
Guidelines:
- Maximum 3 actions per card
- One primary action per screen
- Generous spacing (24px, not 8px)
2. Avoid Urgency
No:
- ❌ Red badges
- ❌ Pulsing animations
- ❌ "New!" labels
Why:
- Reduces anxiety
- Allows focus
- Respects attention
Part 7: Final Polish
Microinteractions
1. Subtle Haptics (Mobile)
When:
- Appreciate/Save actions (light impact)
- Publish post (medium impact)
- Error state (notification feedback)
Implementation:
HapticFeedback.lightImpact(); // Not heavyImpact
Why:
- Confirms action
- But doesn't startle
2. Sound Cues (Optional, OFF by default)
Future:
- Soft "bloom" on appreciate
- Gentle "save" sound
- Muted error tone
Default: OFF Why: Audio = intrusive
Loading States
1. Skeletons, Not Spinners
Design:
ShimmerSkeleton(
child: PostCardSkeleton(),
)
Why:
- Shows structure
- Feels faster
- Less anxiety than spinner
2. Calm Copy
Good:
"Loading..."
"One moment"
Bad:
❌ "Hang tight!"
❌ "Almost there!"
❌ "This won't take long!"
Tone:
- Neutral, not cheerful
- Honest, not performative
Error Handling
1. Gentle Language
Good:
"Couldn't load posts"
"Connection issue"
Bad:
❌ "ERROR: Network failure"
❌ "Oops! Something went wrong!"
❌ "Fatal exception occurred"
2. Clear Recovery
Pattern:
[Error message]
[What happened]
[Action button]
Example:
Couldn't load posts
Check your internet connection.
[ Try Again ]
3. No Blame
Don't:
- ❌ "You're offline"
- ❌ "Invalid input"
Do:
- ✅ "No connection"
- ✅ "Hmm, that didn't work"
Performance
1. Optimize List Rendering
ListView.builder(
itemBuilder: (context, index) {
return RepaintBoundary(
child: PostCard(post: posts[index]),
);
},
)
Why:
- RepaintBoundary = isolate repaints
- Smooth 60fps scroll
2. Cache Feeds Responsibly
- Cache for 5 minutes
- Invalidate on pull-to-refresh
- Respect memory limits
3. No Jank on Scroll
Techniques:
- Lazy load images
- Debounce pagination
- Avoid setState in scroll listener
Summary: What Makes Sojorn Calm
Visual Calm
- Warm neutrals, not cold grays
- Soft shadows (4-8% opacity)
- Generous spacing (24px, not 8px)
- Muted colors (no bright red/blue)
Interaction Calm
- Slow animations (300-400ms)
- Gentle press states (no bounce)
- Subtle haptics (light, not heavy)
- No urgency cues
Content Calm
- Body text is hero (17px, 1.7 line-height)
- Metrics de-emphasized (11px, tertiary)
- Author identity clear but quiet
- No performance pressure
Structural Calm
- Mutual-follow commenting
- Category opt-in
- Block without drama
- Tone guidance without shame
Cognitive Calm
- No mystery (transparency pages)
- No dark patterns (honest UI)
- No jargon (plain language)
- No surprises (predictable navigation)
Result: An app that feels like settling into a good book, not scrolling a frantic timeline.
Enforcement: Design system + custom widgets make it impossible to violate calm principles.
Philosophy: Calm is not a feature. Calm is structural.