Social Features

The ContactsManager SDK provides comprehensive social features to enhance your app with follow relationships and activity feeds. These features enable your users to connect with each other, follow other users of interest, and create an engaging social experience within your application. This guide explains how to implement these features using the SDK.

Follow Relationships

Follow relationships are the foundation of social connectivity in your app. They allow users to curate their network by following specific users, which determines whose content appears in their feeds and activity streams.

Following a User

Allow users to follow other users in your app. Following creates a one-way relationship where a user can see updates from people they follow without requiring mutual connection.

do {
    // Follow a user using their organization-specific user ID
    let result = try await ContactsService.shared.socialService.followUser(
        userId: "user-id"
    )
    
    if let success = result.success, success {
        print("Successfully followed user")
    } else if let alreadyFollowing = result.alreadyFollowing, alreadyFollowing {
        print("Already following this user")
    }
} catch {
    print("Error following user: \(error.localizedDescription)")
}

The followUser method requires:

  • userId: The organization-specific user ID of the user to follow

The response indicates whether the follow action was successful or if the user was already following the specified user.

Unfollowing a User

Users may want to stop following certain users. The unfollow action removes the relationship, preventing that user’s updates from appearing in the user’s feed.

do {
    let result = try await ContactsService.shared.socialService.unfollowUser(
        userId: "user-id"
    )
    
    if let success = result.success, success {
        print("Successfully unfollowed user")
    }
} catch {
    print("Error unfollowing user: \(error.localizedDescription)")
}

The unfollowUser method requires only the organization-specific user ID of the user to unfollow.

Checking Follow Status

Determining if a user is following another user is essential for displaying the correct UI state, such as follow/unfollow buttons on profile pages.

do {
    let status = try await ContactsService.shared.socialService.isFollowingUser(
        userId: "user-id"
    )
    
    if status.isFollowing {
        print("You are following this user")
    } else {
        print("You are not following this user")
    }
} catch {
    print("Error checking follow status: \(error.localizedDescription)")
}

The isFollowingUser method returns a status object with an isFollowing property that indicates the current relationship status. This is useful for updating UI elements that depend on follow status.

Retrieving Followers and Following

Building social experiences often requires displaying lists of followers or users being followed. These methods provide paginated access to these relationships with rich user information.

// Get followers with pagination
do {
    let followers = try await ContactsService.shared.socialService.getFollowers(
        skip: 0, 
        limit: 20
    )
    
    print("You have \(followers.total) followers")
    
    // Display followers
    for relationship in followers.items {
        if let follower = relationship.follower {
            print("Follower: \(follower.fullName ?? "Unknown")")
            
            // Access local contact data if available
            if let localContact = relationship.localContact {
                print("Local contact: \(localContact.displayName ?? "Unknown")")
            }
        }
    }
    
    // Check if there are more followers to load
    if followers.total > followers.limit {
        // Get the next page
        let nextPage = try await ContactsService.shared.socialService.getFollowers(
            skip: followers.limit, 
            limit: 20
        )
    }
} catch {
    print("Error retrieving followers: \(error.localizedDescription)")
}

// Get users the current user is following
do {
    let following = try await ContactsService.shared.socialService.getFollowing(
        skip: 0,
        limit: 20
    )
    
    print("You are following \(following.total) users")
} catch {
    print("Error retrieving following: \(error.localizedDescription)")
}

Both getFollowers and getFollowing methods:

  • Support pagination through skip and limit parameters
  • Return the total number of relationships for implementing UIs with counters
  • Provide both server profile data and local contact information when available
  • Can be used to create scrollable lists in profile views or dedicated followers/following screens

Get followers for a specific user

Sometimes you may want to display followers of a specific user rather than the current user. This is useful for viewing another user’s profile or analyzing social connections.

do {
    let followers = try await ContactsService.shared.socialService.getFollowers(
        userId: "user-id",
        skip: 0, 
        limit: 20
    )
    
    print("User has \(followers.total) followers")
} catch {
    print("Error retrieving user followers: \(error.localizedDescription)")
}

By providing the optional userId parameter, you can retrieve followers for any user in the system, not just the authenticated user.

Get Mutual Follows

Mutual follows (users who follow the current user and whom the current user follows back) often represent stronger social connections. Displaying mutual follows can help users identify their most engaged connections.

do {
    let mutualFollows = try await ContactsService.shared.socialService.getMutualFollows(
        skip: 0,
        limit: 20
    )
    
    print("You have \(mutualFollows.total) mutual follows")
} catch {
    print("Error retrieving mutual follows: \(error.localizedDescription)")
}

The getMutualFollows method returns bidirectional relationships where both users follow each other. This is useful for implementing “close friends” features or prioritizing content from mutual connections.

Activity Feeds

The SDK provides activity feeds to display content from users. Activity feeds create a dynamic social experience where users can see updates, events, and actions from people they follow. For detailed implementation of feed features and events, please refer to the Events documentation.

Building a Social Activity Feed

Here’s an example of building a social activity feed with SwiftUI, demonstrating a complete implementation of a scrollable feed with pagination, error handling, and empty states.

This is a comprehensive example that shows a complete implementation. The core concepts can be adapted to fit your app’s design and requirements.

ActivityFeedView.swift
struct ActivityFeedView: View {
    @State private var feedEvents: [SocialEvent] = []
    @State private var isLoading = true
    @State private var error: Error?
    @State private var currentPage = 0
    @State private var hasMoreEvents = true
    
    private let pageSize = 20
    
    var body: some View {
        NavigationView {
            Group {
                if isLoading && feedEvents.isEmpty {
                    ProgressView("Loading feed...")
                } else if let error = error {
                    VStack {
                        Text("Error loading feed")
                            .font(.headline)
                        Text(error.localizedDescription)
                            .foregroundColor(.red)
                        Button("Try Again") {
                            loadFeed(refresh: true)
                        }
                        .padding()
                    }
                } else if feedEvents.isEmpty {
                    VStack {
                        Image(systemName: "person.2.slash")
                            .font(.system(size: 48))
                            .foregroundColor(.gray)
                            .padding()
                        
                        Text("No events in your feed")
                            .font(.headline)
                        
                        Text("Follow more people to see their events")
                            .foregroundColor(.gray)
                        
                        Button("Find People to Follow") {
                            // Navigate to recommendations view
                        }
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                        .padding(.top)
                    }
                    .padding()
                } else {
                    List {
                        ForEach(feedEvents, id: \.id) { event in
                            EventRow(event: event)
                        }
                        
                        if hasMoreEvents {
                            ProgressView()
                                .frame(maxWidth: .infinity, alignment: .center)
                                .onAppear {
                                    loadMoreEvents()
                                }
                        }
                    }
                    .refreshable {
                        await refreshFeed()
                    }
                }
            }
            .navigationTitle("Activity Feed")
            .onAppear {
                if feedEvents.isEmpty {
                    loadFeed(refresh: true)
                }
            }
        }
    }
    
    private func loadFeed(refresh: Bool) {
        if refresh {
            currentPage = 0
            feedEvents = []
        }
        
        isLoading = true
        error = nil
        
        Task {
            do {
                let feed = try await ContactsService.shared.socialService.getFeed(
                    skip: currentPage * pageSize,
                    limit: pageSize
                )
                
                await MainActor.run {
                    if refresh {
                        feedEvents = feed.items
                    } else {
                        feedEvents.append(contentsOf: feed.items)
                    }
                    
                    hasMoreEvents = feed.items.count >= pageSize && feed.total > feedEvents.count
                    currentPage += 1
                    isLoading = false
                }
            } catch {
                await MainActor.run {
                    self.error = error
                    self.isLoading = false
                }
            }
        }
    }
    
    private func loadMoreEvents() {
        if !isLoading && hasMoreEvents {
            loadFeed(refresh: false)
        }
    }
    
    private func refreshFeed() async {
        await MainActor.run {
            currentPage = 0
            feedEvents = []
            isLoading = true
            error = nil
        }
        
        do {
            let feed = try await ContactsService.shared.socialService.getFeed(
                skip: 0,
                limit: pageSize
            )
            
            await MainActor.run {
                feedEvents = feed.items
                hasMoreEvents = feed.items.count >= pageSize && feed.total > feedEvents.count
                currentPage = 1
                isLoading = false
            }
        } catch {
            await MainActor.run {
                self.error = error
                self.isLoading = false
            }
        }
    }
}

struct EventRow: View {
    let event: SocialEvent
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            // Event header with creator info
            HStack {
                // Avatar placeholder
                Circle()
                    .fill(Color.gray.opacity(0.3))
                    .frame(width: 40, height: 40)
                
                VStack(alignment: .leading) {
                    Text(event.creatorName ?? "Unknown")
                        .font(.headline)
                    
                    Text(formattedEventTime)
                        .font(.caption)
                        .foregroundColor(.gray)
                }
                
                Spacer()
            }
            
            // Event content
            VStack(alignment: .leading, spacing: 4) {
                Text(event.title)
                    .font(.title3)
                    .fontWeight(.bold)
                
                if let description = event.description, !description.isEmpty {
                    Text(description)
                        .font(.body)
                        .lineLimit(3)
                }
                
                if let location = event.location, !location.isEmpty {
                    HStack {
                        Image(systemName: "location.fill")
                            .font(.caption)
                        Text(location)
                            .font(.subheadline)
                    }
                    .foregroundColor(.gray)
                }
            }
            .padding(.vertical, 4)
            
            // Action buttons
            HStack {
                Button(action: {
                    // Add to calendar
                }) {
                    Label("Calendar", systemImage: "calendar.badge.plus")
                }
                .buttonStyle(.bordered)
                
                Spacer()
                
                Button(action: {
                    // Share event
                }) {
                    Label("Share", systemImage: "square.and.arrow.up")
                }
                .buttonStyle(.bordered)
            }
            .padding(.top, 4)
        }
        .padding(.vertical, 8)
    }
    
    private var formattedEventTime: String {
        guard let startTime = event.startTime else {
            return "No date"
        }
        
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .short
        
        return formatter.string(from: startTime)
    }
}

This example demonstrates how to:

  • Implement pagination for efficient data loading
  • Handle different UI states (loading, error, empty, populated)
  • Create visually appealing event cards
  • Implement pull-to-refresh functionality
  • Automatically load more content when scrolling to the bottom

Best Practices for Social Features

  1. Handle Network Failures: Social features rely on network communication, so implement proper error handling with user-friendly error messages and retry options to ensure a smooth experience even with spotty connectivity.

  2. Implement Pagination: Use skip and limit parameters for better performance with large datasets. Avoid loading the entire dataset at once, which can cause performance issues and waste bandwidth.

  3. Optimize for Offline Use: Cache social data for a better offline experience. Consider storing the most recent feed items locally so users can view content even without an internet connection.

  4. Follow Privacy Guidelines: Clearly communicate to users how their data is used. Implement proper permissions for accessing user information and provide clear opt-in/opt-out mechanisms for social features.

  5. Support Pull-to-Refresh: Implement refreshable lists for up-to-date content, as social feeds frequently change and users expect to be able to check for new content easily.

  6. Pre-fetch Content: Load the next page of content before the user reaches the end of the list to create a seamless scrolling experience without visible loading indicators.

Troubleshooting

Common Issues

  1. Authentication Errors

    • Ensure the SDK is properly initialized with a valid token before attempting social operations
    • Check that the token hasn’t expired, and implement proper token refresh mechanisms
    • Verify that users have the necessary permissions to perform social actions
  2. Missing or Incomplete Data

    • Verify that the necessary user data has been properly synced before enabling social features
    • Check that users have complete profiles with required fields filled out
    • Ensure proper initialization of the ContactsService before accessing social features
  3. Performance Issues

    • Implement proper pagination in all list views to avoid loading excessive data
    • Use background fetch for keeping social data up-to-date without blocking the UI
    • Cache commonly accessed data locally to reduce network requests and improve responsiveness