Skip to content

Chatter Publisher with Autosave Implementation

Table of Contents

Overview

This implementation provides a custom Lightning Web Component that replaces the standard Chatter publisher with autosave capabilities to prevent loss of drafted messages when composing updates to activity groups.

The Problem

Members were losing composed Chatter posts when:

  • Browser windows needed updates or crashed
  • Users navigated away from the page accidentally
  • Sessions timed out requiring re-login
  • Browsers were closed without posting

The standard Salesforce Chatter publisher in Experience Cloud provides no draft saving functionality, resulting in frustration and wasted time, especially for longer or more thoughtful messages to activity groups.

The Solution

A custom Lightning Web Component (chatterPublisherWithAutosave) that provides:

  1. Automatic Draft Saving - Saves to browser localStorage after typing stops (configurable delay, default 3 seconds)
  2. Post and Poll Tabs - Post (rich text) and Poll (question + 2–10 choices) with separate drafts per tab
  3. Rich Text Support - Full formatting preservation for posts (bold, italic, lists, links, etc.)
  4. Draft Restoration - Prompts users to restore saved post or poll drafts when returning to pages
  5. Navigation Warnings - Warns users before leaving with unsaved changes
  6. Visual Feedback - Shows "Draft saved" briefly; message disappears after a short delay (configurable, default 3 seconds)
  7. Draft Expiration - Auto-expires drafts older than 7 days
  8. Per-Group Drafts - Each Chatter group maintains its own separate post and poll drafts

Goals

  1. Prevent loss of composed messages across all user scenarios
  2. Support rich text formatting in drafts
  3. Provide seamless user experience with minimal friction
  4. Work reliably across browser sessions and crashes
  5. Ensure comprehensive test coverage for Apex controllers

Architecture

Component Overview

Chatter Autosave System
│
├── chatterPublisherWithAutosave (Lightning Web Component)
│   ├── chatterPublisherWithAutosave.js (Component logic)
│   ├── chatterPublisherWithAutosave.html (Template)
│   ├── chatterPublisherWithAutosave.css (Styling)
│   └── chatterPublisherWithAutosave.js-meta.xml (Metadata)
│
├── ChatterPublisherController.cls (Apex Controller)
│   └── ChatterPublisherControllerTest.cls (Test Class)
│
└── Configuration
    └── Experience Builder (Component Placement)

Key Design Decisions

Lightning Web Component Over Aura: We chose LWC because:

  • Modern framework with better performance
  • Native support for localStorage and browser APIs
  • Better developer experience and tooling
  • lightning-input-rich-text component provides robust formatting
  • Easier to maintain and extend

localStorage for Draft Persistence: We chose browser localStorage over server-side storage because:

  • Instant save with no network latency
  • Works offline and survives browser restarts
  • No additional database objects or storage limits needed
  • Simpler implementation and faster user experience
  • Drafts automatically scoped to device/browser (privacy benefit)

Debounced Autosave: Draft saving uses a configurable debounce (see AUTOSAVE_DELAY_MS in the LWC, default 3 seconds) to:

  • Avoid excessive localStorage writes
  • Balance responsiveness with performance
  • Match user expectations from other editing tools

Rich Text HTML Storage: We store full HTML rather than plain text to:

  • Preserve all formatting applied by users
  • Support restoration of complex content
  • Allow for future enhancement of formatting options

Implementation Details

localStorage Key Structure

Drafts are stored with keys in the format:

  • Post drafts: chatterDraft_<groupId>
  • Poll drafts: chatterPollDraft_<groupId>

Each Chatter group maintains its own separate post and poll drafts, allowing users to have in-progress posts and polls for multiple groups simultaneously.

Draft Data Structure

Post draft:

{
  "content": "<p>Draft message content with HTML</p>",
  "timestamp": "2026-02-08T10:30:00.000Z",
  "groupId": "0F9xxxxxxxxxx"
}

Poll draft:

{
  "type": "poll",
  "question": "Poll question text",
  "choices": ["Choice 1", "Choice 2", "Choice 3"],
  "timestamp": "2026-02-08T10:30:00.000Z",
  "groupId": "0F9xxxxxxxxxx"
}

Autosave Flow

Post tab:

  1. User types in rich text editor (or edits in Poll tab: question or choices)
  2. Component detects change via onchange event
  3. Debounce timer starts (duration from AUTOSAVE_DELAY_MS, default 3 seconds)
  4. If user stops typing, timer fires and saves to localStorage
  5. Visual indicator shows "Draft saved" briefly, then disappears after DRAFT_SAVED_MESSAGE_DURATION_MS (default 3 seconds)
  6. On page load, component checks for existing draft (post or poll)
  7. If found and not expired, shows restoration modal
  8. User can restore or discard draft

Poll tab: Same debounce and save flow; poll drafts use chatterPollDraft_<groupId> and include question and choices.

  1. User starts typing (sets hasUnsavedChanges = true)
  2. On successful save to localStorage, flag cleared
  3. beforeunload event listener checks flag
  4. If unsaved changes exist, browser shows native confirmation dialog
  5. User can choose to stay or leave page

Draft Expiration

  • Drafts expire after 7 days
  • Cleanup runs on component initialization
  • Expired drafts are automatically removed from localStorage
  • Prevents storage bloat from abandoned drafts

Component Details

ChatterPublisherWithAutosave (LWC)

Purpose: Provides custom Chatter publisher with autosave functionality

Key constants (in component JS):

  • AUTOSAVE_DELAY_MS - Debounce delay before saving draft (default 3000 ms)
  • DRAFT_SAVED_MESSAGE_DURATION_MS - How long "Draft saved" stays visible (default 3000 ms)
  • DRAFT_EXPIRATION_DAYS - Drafts older than this are expired (7)

Key Properties:

@api groupId; // Can be set explicitly or auto-detected from page
_internalGroupId; // Auto-detected from CurrentPageReference
effectiveGroupId; // Computed property that returns groupId || _internalGroupId
activeTab; // "post" | "poll"

Key Methods:

handleTextChange(event);

Responds to text changes in the rich text editor (Post tab):

  1. Updates draftContent with new value
  2. Sets hasUnsavedChanges flag
  3. Clears existing debounce timeout
  4. Sets new timeout for autosave (duration from AUTOSAVE_DELAY_MS)
saveDraftToLocalStorage();

Saves current post draft to browser localStorage:

  1. Validates content is not empty
  2. Creates draft object with content, timestamp, groupId
  3. Stores in localStorage with prefixed key
  4. Shows "Draft saved" briefly (cleared after DRAFT_SAVED_MESSAGE_DURATION_MS)
  5. Clears hasUnsavedChanges flag

Poll drafts use schedulePollDraftSave() and savePollDraftToLocalStorage() with the same debounce and temporary message behavior.

loadDraftFromLocalStorage();

Loads existing draft from localStorage (post and/or poll):

  1. Retrieves draft by groupId key (post: chatterDraft_, poll: chatterPollDraft_)
  2. Checks if draft is expired (>7 days)
  3. If expired, deletes and returns
  4. If valid, shows restoration modal with relative timestamp (restore or discard)
handlePost();

Posts content to Chatter via Apex:

  1. Validates content is not empty
  2. Calls ChatterPublisherController.postToChatter
  3. On success, clears draft and shows success toast
  4. Reloads page to show new post in feed
  5. On error, shows error toast and keeps draft
beforeUnloadHandler(event);

Warns user before leaving with unsaved changes:

  1. Checks hasUnsavedChanges flag and content not empty
  2. If true, prevents default navigation
  3. Sets returnValue to trigger browser confirmation dialog

Helper Methods:

  • cleanupExpiredDrafts() - Removes expired post and poll drafts (checks both key prefixes)
  • getDraftKey() / getPollDraftKey() - Return localStorage keys for current group
  • isContentNotEmpty() - Strips HTML and checks for actual text
  • isDraftExpired() - Checks if draft is older than 7 days
  • getRelativeTime() - Formats timestamp as "just now", "X minutes ago", etc.
  • showToast() - Displays lightning toast messages

ChatterPublisherController (Apex)

Purpose: Handles posting messages to Chatter groups via ConnectApi

Key Methods:

@AuraEnabled
public static String postToChatter(String groupId, String content)

Posts a text message to a Chatter group:

  1. Validates groupId and content are provided
  2. Verifies Chatter group exists and user has access
  3. Converts rich text HTML to plain text
  4. Builds ConnectApi.FeedItemInput
  5. Posts via ConnectApi.ChatterFeeds.postFeedElement
  6. Returns feed item ID on success
@AuraEnabled
public static String postPollToChatter(String groupId, String question, List<String> choices)

Posts a poll to a Chatter group:

  1. Validates groupId, question, and at least 2 non-empty choices (max 10)
  2. Verifies Chatter group exists and user has access
  3. Builds ConnectApi.FeedItemInput with body (question) and PollCapabilityInput (choices)
  4. Posts via ConnectApi.ChatterFeeds.postFeedElement
  5. Returns feed element ID on success (or mock ID in test context)
private static Id getNetworkId()

Gets the network ID for community context:

  1. Tries Network.getNetworkId() first
  2. Falls back to querying for "Spokane Mountaineers" network
  3. Returns network ID or null
private static String stripHtmlTags(String html)

Converts rich text HTML to plain text:

  1. Converts <br> and </p> tags to newlines
  2. Converts list items to bullet points
  3. Removes all other HTML tags
  4. Decodes HTML entities (&, <, etc.)
  5. Cleans up multiple consecutive newlines

Testing Strategy

Test Coverage Goals

ChatterPublisherController: Achieved 100% code coverage

Test Scenarios Covered:

  1. Successful post and poll posting:

    • Plain text content
    • Rich text with formatting
    • Content with HTML entities
    • Content with line breaks
    • Content with lists
    • Long content
    • Content with special characters
    • Poll: valid question and 2–10 choices; blank choices filtered out
  2. Error Handling:

    • Null group ID
    • Empty group ID
    • Null content
    • Empty content
    • Invalid/non-existent group ID
    • Poll: null/blank groupId, blank question, too few choices, too many choices
  3. Edge Cases:

    • Group ID validation
    • HTML to text conversion
    • Entity decoding
    • List formatting

Test Data Strategy

Tests use:

  • Dynamic Chatter group creation with unique names (timestamp suffix)
  • Query for existing Public Chatter groups when possible
  • Proper use of Test.startTest() and Test.stopTest()
  • Comprehensive assertions for positive and negative cases
  • Mock HTML content representing real-world usage

Running Tests

# Run all tests for the controller
sf apex run test --tests ChatterPublisherControllerTest --target-org staging --code-coverage --result-format human

# Check code coverage
sf apex get test --code-coverage --target-org staging

Deployment Instructions

Prerequisites

  1. Salesforce CLI installed
  2. Access to staging and production orgs
  3. Git repository cloned locally
  4. Feature branch created: feature/chatter-autosave

Quick Deploy (New Components Only)

To deploy only the Chatter autosave components and run only their tests (faster than a full project deploy):

sf project deploy start \
  --target-org staging \
  --source-dir force-app/main/default/classes/ChatterPublisherController.cls \
  --source-dir force-app/main/default/classes/ChatterPublisherControllerTest.cls \
  --source-dir force-app/main/default/lwc/chatterPublisherWithAutosave \
  --test-level RunSpecifiedTests \
  --tests ChatterPublisherControllerTest \
  --wait 10
  • --source-dir (repeated): Deploys only these three items (each Apex class includes its .cls-meta.xml).
  • --test-level RunSpecifiedTests: Only ChatterPublisherControllerTest runs.
  • --wait 10: Wait up to 10 minutes for the deployment to finish.

Use the same command for production by changing --target-org (e.g. --target-org production).

Step 1: Deploy to Staging (Full Project)

# Deploy the component and Apex classes to staging
sf project deploy start --target-org staging

# Monitor deployment
sf project deploy report --target-org staging

Step 2: Run Apex Tests

# Run tests and verify >75% coverage
sf apex run test --tests ChatterPublisherControllerTest \
  --target-org staging \
  --code-coverage \
  --result-format human \
  --wait 10

# Get detailed coverage report
sf apex get test --code-coverage --target-org staging

Expected output:

  • All test methods passing
  • Code coverage >75% for ChatterPublisherController
  • No test failures or errors

Step 3: Add Component to Experience Cloud

  1. Open Experience Builder:

    • Navigate to Digital Experiences > All Sites
    • Find "Spokane Mountaineers" site
    • Click "Builder"
  2. Navigate to Group Detail Page:

    • In Experience Builder, go to any Chatter group page
    • Or configure via Settings > Theme > Group Pages template
  3. Add Component:

    • In left sidebar, find "Custom Components"
    • Drag "Chatter Publisher with Autosave" onto the page
    • Position it above the standard Chatter feed
    • Leave "Chatter Group ID" property blank (auto-detects from page)
  4. Publish Changes:

    • Click "Publish" button
    • Confirm publication to make changes live

Step 4: Test in Staging

Test Scenarios:

  1. Draft Saving:

    • Navigate to a Chatter group page in staging
    • Start typing a message with formatting (Post tab) or question/choices (Poll tab)
    • Wait a few seconds (autosave delay, default 3 seconds)
    • Verify "Draft saved" appears briefly, then disappears
    • Check browser console: localStorage.getItem('chatterDraft_<groupId>') or chatterPollDraft_<groupId> for poll
  2. Draft Restoration:

    • With a saved post or poll draft, refresh the page
    • Verify restoration modal appears (post or poll wording)
    • Click "Restore Draft"
    • Confirm content is restored (post formatting or poll question/choices intact)
  3. Navigation Warning:

    • Start typing without waiting for autosave
    • Try to navigate away or close tab
    • Verify browser shows confirmation dialog
  4. Posting:

    • Compose a message (Post) or poll (Poll tab), then click "Share"
    • Verify post or poll appears in Chatter feed
    • Confirm draft is cleared from localStorage
    • "Draft saved" message is temporary and clears on its own after the configured duration
  5. Draft Expiration:

    • Manually create expired draft in console:
      const groupId = "0F9..."; // Current group ID
      const expiredDraft = {
          content: "<p>Old draft</p>",
          timestamp: new Date(
              Date.now() - 8 * 24 * 60 * 60 * 1000
          ).toISOString(), // 8 days ago
          groupId: groupId
      };
      localStorage.setItem(
          `chatterDraft_${groupId}`,
          JSON.stringify(expiredDraft)
      );
      
    • Refresh page
    • Verify no restoration modal appears
    • Check localStorage to confirm draft was removed

Step 5: Deploy to Production

After successful staging testing:

# Switch to production org
sf config set target-org production

# Deploy
sf project deploy start --target-org production

# Run tests in production
sf apex run test --tests ChatterPublisherControllerTest \
  --target-org production \
  --code-coverage \
  --result-format human \
  --wait 10

Step 6: Rollout Strategy

Phased Approach:

  1. Deploy component to one test group (e.g., Ecomm) first
  2. Monitor for 1-2 days
  3. Roll out to all activity group pages
  4. Communicate to members about new autosave feature

Rollback Plan:

If issues arise:

  1. Remove component from Experience Builder (immediate)
  2. Members revert to standard Chatter publisher
  3. No data loss (drafts remain in localStorage)
  4. Fix issues and redeploy

Lessons Learned

What Worked Well

localStorage for Persistence: Using browser localStorage provided instant saves with no network latency and survived browser crashes perfectly. The approach proved more reliable than we initially expected, with no storage quota issues even with rich HTML content.

Debounced Autosave: The configurable debounce (e.g. 3 seconds) strikes a balance between responsiveness and performance. Users see a brief "Draft saved" confirmation that disappears after a short delay, avoiding clutter.

Rich Text Component: The lightning-input-rich-text component provided robust formatting out of the box. The toolbar was familiar to users from other editing tools, reducing the learning curve.

ESLint Enforcement: Pre-commit hooks caught several issues early:

  • Invalid @api property reassignment
  • Missing return statements
  • Async operation restrictions
  • innerHTML security concerns

These catches prevented bugs from reaching production.

Comprehensive Test Coverage: Achieving 100% coverage for the Apex controller gave us confidence to deploy. Edge cases like null values, empty strings, and invalid group IDs were all covered.

Challenges Overcome

LWC Platform Restrictions: Hit several ESLint rules specific to LWC:

  • @lwc/lwc/no-api-reassignments - Can't reassign @api properties
    • Solution: Created _internalGroupId private property and effectiveGroupId getter
  • @lwc/lwc/no-async-operation - Can't use setTimeout without eslint-disable
    • Solution: Added inline eslint-disable comment with justification
  • @lwc/lwc/no-inner-html - Can't use innerHTML for security
    • Solution: Added eslint-disable where needed for HTML parsing in helper method

ConnectApi HTML Limitations: The Connect API doesn't support rich HTML posting directly. Initial implementation tried to post HTML but Chatter stripped formatting.

  • Solution: Built stripHtmlTags method to intelligently convert HTML to plain text while preserving structure (line breaks, lists)

beforeunload Event Handling: The browser's beforeunload event requires returning a value, but ESLint flagged missing return in else branch.

  • Solution: Added explicit return undefined for consistency

Draft Restoration UX: Initial implementation restored drafts automatically, which was jarring if users didn't remember they had a draft.

  • Solution: Added modal dialog with "Restore" vs "Discard" options, showing timestamp so users can decide

Multiple Chatter Groups: Needed to handle users composing drafts for multiple groups simultaneously.

  • Solution: Keyed drafts by groupId, allowing independent drafts per group

Future Considerations

Server-Side Draft Backup: Consider adding optional server-side draft storage for:

  • Cross-device access (draft on desktop, restore on mobile)
  • Draft sharing/collaboration
  • Audit trail of draft history
  • Recovery if localStorage is cleared

This could be implemented as an enhancement without changing the current localStorage-first approach.

Rich Text Posting: Explore options for preserving rich text formatting in Chatter posts:

  • ConnectApi rich text segments (may support some formatting)
  • Markdown conversion for structured content
  • Image paste support

@Mention and #Hashtag Preservation: Current implementation doesn't preserve Chatter-specific syntax. Future enhancement could:

  • Parse @mentions from HTML
  • Convert to ConnectApi.MentionSegmentInput
  • Parse #hashtags and convert to ConnectApi.HashtagSegmentInput

Draft Versioning: For long-form posts, users might appreciate:

  • Multiple draft versions
  • Undo/redo functionality
  • Draft comparison view

Analytics Integration: Track usage metrics:

  • How often drafts are saved
  • How often drafts are restored vs discarded
  • Average draft lifespan
  • Most common draft content lengths

Mobile Optimization: While the component works on mobile, consider:

  • Touch-optimized toolbar
  • Simplified formatting options for small screens
  • Better modal sizing for mobile viewports

Accessibility Improvements:

  • ARIA labels for screen readers
  • Keyboard shortcuts for common actions
  • Focus management in restoration modal
  • High contrast mode support

Code References

Lightning Web Component:

  • force-app/main/default/lwc/chatterPublisherWithAutosave/chatterPublisherWithAutosave.js
  • force-app/main/default/lwc/chatterPublisherWithAutosave/chatterPublisherWithAutosave.html
  • force-app/main/default/lwc/chatterPublisherWithAutosave/chatterPublisherWithAutosave.css
  • force-app/main/default/lwc/chatterPublisherWithAutosave/chatterPublisherWithAutosave.js-meta.xml

Apex Classes:

  • force-app/main/default/classes/ChatterPublisherController.cls
  • force-app/main/default/classes/ChatterPublisherController.cls-meta.xml
  • force-app/main/default/classes/ChatterPublisherControllerTest.cls
  • force-app/main/default/classes/ChatterPublisherControllerTest.cls-meta.xml

Configuration:

  • force-app/main/default/lwc/.eslintrc.json - ESLint configuration for LWC

Issue Reference

Addresses Issue #10: No Autosave When Composing Messages