Unified Profile
With Unified Profile, profile taps inside the community open your own profile screens, and your app can open and read members' activity, profile, community data and comments.
Android ≥ 1.13.0iOS ≥ 1.13.0Flutter ≥ 1.13.0React Native ≥ 1.13.0Unity ≥ 1.13.0
Before you begin
- Link your users to the SDK with your own account system: see Link your user to the SDK. The id you pass there is the client user id this page refers to.
- Display the community: see Display the community.
How it works
By default, tapping a member's profile inside the community opens the SDK profile screens. With Unified Profile, the SDK hands each tap back to your app with the tapped member's client user id, so you open your own profile screen. The SDK never hands you an Octopus id in this callback.
Unified Profile also:
- Replaces the connected user's avatar on the main feed's floating button with an Activity icon. It opens the Activity screen.
- Lets your profile screens read a member's community data: message count and gamification level.
- Lets you open a member's activity screen from your own UI.
When Unified Profile is on, profile taps go here:
| Tapped profile | What opens |
|---|---|
| A member with a client user id, including the connected user's own name or avatar in a post or comment | Your callback, with that client user id |
| A member without a client user id: a guest, or a profile created from the back office | The SDK member activity screen. Your callback never gets a null id. |
| The floating button on the main feed | The SDK Activity screen |
Activation
Unified Profile turns on only when both of these are true:
- Your community exposes client user ids. The Octopus team turns this on for each community: ask your Octopus contact.
- Your app sets the profile tap callback: see Route profile taps.
Until both are true, the SDK keeps its own profile screens everywhere. You can ship the callback before the community setting is on, or the other way around.
Route profile taps to your own profile screen
Set the callback, then open your own profile screen for the client user id it receives.
- Android
- iOS
- Flutter
- React Native
- Unity
Parameters:
onNavigateToProfile: ((clientUserId: String) -> Unit)?: called when the user taps any profile in the community. Leave itnull, the default, to keep the SDK profile screens. It exists onoctopusComposables,OctopusHomeScreen,OctopusHomeContent,OctopusPostDetailsContentandOctopusGroupDetailsContent.
NavHost(navController = navController, startDestination = HomeRoute) {
octopusComposables(
navController = navController,
onNavigateToLogin = { navController.navigate(LoginRoute) },
onNavigateToProfileEdit = { fieldToEdit -> navController.navigate(EditProfileRoute) },
onNavigateToProfile = { clientUserId ->
navController.navigate(YourProfileRoute(userId = clientUserId))
},
) { backStackEntry, content ->
OctopusTheme {
content()
}
}
}
Parameters:
onNavigateToProfileCallback: ((_ clientUserId: String) -> Void)?: called when the user taps any profile in the community. Passnilto go back to the SDK profile screens.
octopus.set(onNavigateToProfileCallback: { clientUserId in
// Open your own profile screen for clientUserId
})
Parameters:
onNavigateToProfile: void Function(String clientUserId)?: called when the user taps any profile in the community. Leave itnull, the default, to keep the SDK profile screens. It exists onOctopusHomeScreen,OctopusHomeContent,OctopusPostDetailsScreen,OctopusGroupDetailsScreen, and on theshowOctopusHomeScreenandopenNotificationinstance methods.
OctopusHomeScreen(
onNavigateToLogin: () {},
onModifyUser: (String? field) {},
onNavigateToProfile: (clientUserId) {
Navigator.of(context).push(yourProfileRoute(clientUserId));
},
)
The SDK reads whether the callback is set when the view is created. Replacing one callback with another applies at once, but switching between null and a callback applies on the next mount: change the widget key to toggle it at runtime. On iOS, mounting a second Octopus view applies its choice to the first one too, so wire every view the same way.
Opt in with interceptProfileTaps on openUI() or <OctopusUIView>, then listen with addNavigateToProfileListener:
import { addNavigateToProfileListener, openUI } from '@octopus-community/react-native';
const subscription = addNavigateToProfileListener(({ clientUserId }) => {
navigation.navigate('Profile', { userId: clientUserId });
});
await openUI({ interceptProfileTaps: true });
// Or: <OctopusUIView interceptProfileTaps />
Parameters:
NavigateToProfileHandler: Action<string>: receives the client user id on the Unity main thread, after the SDK closes its UI. Set it tonullto go back to the SDK profile screens.
OctopusSDK.NavigateToProfileHandler = clientUserId =>
{
// Open your own profile screen for clientUserId
};
// Later, to go back to the SDK profile screens
OctopusSDK.NavigateToProfileHandler = null;
OnNavigateToProfile is a notification event only. Subscribing to it does not turn on Unified Profile: set NavigateToProfileHandler.
The connected user's Activity screen
When Unified Profile is on, the connected user's avatar on the main feed's floating button becomes an Activity icon. It opens the Activity screen, with Notifications and Posts tabs and a Comments tab ≥ 1.14.0, and no profile header. Its overflow menu holds:
- View my profile: calls your profile tap callback with the connected user's client user id. Shown for a signed-in member with a client user id.
- Edit my profile: calls your profile edit callback, described below. Shown when that callback is set and View my profile is shown.
- The community's legal links: community guidelines, privacy policy and terms of use.
- Report inappropriate content.
Choose the initial profile or Activity tab
Android ≥ 1.14.0iOS not availableFlutter not availableReact Native not availableUnity not available
- Android
- iOS
- Flutter
- React Native
- Unity
With octopusComposables in your NavHost, navigate to a predefined OctopusDestination rather than a raw tab index. Each destination has its own tab order:
| Destination | Posts | Comments | Notifications |
|---|---|---|---|
OctopusDestination.CurrentUserProfileSummary | 0 | 1 | 2 |
OctopusDestination.Activity | 2 | 1 | 0 |
Parameters, when you build a destination yourself:
selectedTabIndex: Int = 0: initial tab, with the mapping above.Activity.userId: String? = nullandActivity.clientUserId: String? = null: leave both unset for the connected user. For another member, use the member activity helpers: they ignoreselectedTabIndex.
import com.octopuscommunity.sdk.ui.OctopusDestination
// The connected user's profile, on Comments
navController.navigate(OctopusDestination.CurrentUserProfileSummary.Comments)
// The connected user's Activity, on Posts
navController.navigate(OctopusDestination.Activity.Posts)
≥ 1.14.0 The indices changed when the Comments tab arrived: 1 now opens Comments on both destinations. Profile Notifications moved from 1 to 2, and Activity Posts from 1 to 2. Prefer the predefined destinations: CurrentUserProfileSummary provides Posts, Comments and Notifications, Activity provides Posts and Notifications, and Activity(selectedTabIndex = 1) opens its Comments tab.
Choosing the initial profile or Activity tab is not yet available on iOS.
Choosing the initial profile or Activity tab is not yet available on Flutter.
Choosing the initial profile or Activity tab is not yet available on React Native.
Choosing the initial profile or Activity tab is not yet available on Unity.
Open your profile editor from the Activity screen
The Edit my profile menu item calls a profile edit callback with a null field: open your full profile editor.
- Android
- iOS
- Flutter
- React Native
- Unity
Parameters:
onNavigateToProfileEdit: ((fieldToEdit: ProfileField?) -> Unit)?: the same parameter ofoctopusComposablesandOctopusHomeContentdescribed in Display the community.
octopusComposables(
navController = navController,
onNavigateToProfileEdit = { fieldToEdit ->
// null when opened from the Activity screen: open your full profile editor
navController.navigate(EditProfileRoute)
},
onNavigateToProfile = { clientUserId ->
navController.navigate(YourProfileRoute(userId = clientUserId))
},
)
Parameters:
onNavigateToProfileEditCallback: ((_ fieldToEdit: ConnectionMode.SSOConfiguration.ProfileField?) -> Void)?: called when the connected user asks to edit their profile from the Activity screen. It works in both.octopusand.ssoconnection modes, and is separate from the SSOmodifyUsercallback. Passnilto remove it, which also hides Edit my profile.
octopus.set(onNavigateToProfileEditCallback: { fieldToEdit in
// nil when opened from the Activity screen: open your full profile editor
})
Parameters:
onModifyUser: Function(String?)?: the same parameter ofOctopusHomeScreenandOctopusHomeContentdescribed in Display the community.
OctopusHomeScreen(
onNavigateToLogin: () {},
onModifyUser: (String? field) {
// null when opened from the Activity screen: open your full profile editor
},
onNavigateToProfile: (clientUserId) {},
)
Set onModifyUser whenever you set onNavigateToProfile. On Android, Edit my profile shows either way, and does nothing without the callback.
The item sends the editUser event described in Link your user to the SDK:
import { addEditUserListener } from '@octopus-community/react-native';
const subscription = addEditUserListener(({ fieldToEdit }) => {
// null when opened from the Activity screen: open your full profile editor
});
The item raises the OnModifyUser event described in Link your user to the SDK:
OctopusSDK.OnModifyUser += (ProfileField? field) =>
{
// null when opened from the Activity screen: open your full profile editor
};
≥ 1.14.0 On iOS, Edit my profile shows only while OnModifyUser has a subscriber. On Android it always shows, and does nothing without a subscriber. Keep a subscriber and both platforms behave the same.
Customize the Activity icon
The Activity icon follows the rules of the other customizable icons: the SDK tints it, and 24×24 fits best. Without an override, the SDK shows its bell icon.
- Android
- iOS
- Flutter
- React Native
- Unity
import com.octopuscommunity.sdk.ui.OctopusIconsDefaults
import com.octopuscommunity.sdk.ui.OctopusImagesDefaults
import com.octopuscommunity.sdk.ui.OctopusTheme
OctopusTheme(
images = OctopusImagesDefaults.images(
icons = OctopusIconsDefaults.icons(
activityButton = { painterResource(R.drawable.your_activity_icon) },
),
),
) {
OctopusHomeContent(navController = navController)
}
import OctopusUI
let theme = OctopusTheme(
assets: .init(
icons: .init(
common: .init(
activityButton: UIImage(named: "myActivityIcon")
)
)
)
)
Set common.activityButton in OctopusTheme.icons:
final activity = await OctopusIconSource.asset('assets/icons/my_activity_icon.png');
final theme = OctopusTheme(
icons: OctopusIcons(
common: OctopusCommonIcons(activityButton: activity),
),
);
Set common.activityButton in theme.icons, passed to initialize():
import { Image } from 'react-native';
import type { OctopusTheme } from '@octopus-community/react-native';
const theme: OctopusTheme = {
icons: {
common: {
activityButton: Image.resolveAssetSource(require('./assets/icons/my_activity_icon.png')),
},
},
};
Set the CommonActivityButton slot with OctopusSDK.SetIcons:
OctopusSDK.SetIcons(new OctopusIcons()
.Set(OctopusIconSlot.CommonActivityButton,
new OctopusIcon("my_activity_icon", "Data/Raw/my_activity_icon.png")));
Open a member's activity screen directly
From your own profile screen, open the SDK member activity screen. It shows the member's posts, plus a Comments tab ≥ 1.14.0 when your community shows members' comments on their profile, which is a back-office setting. Identify the member in one of two ways:
- By their client user id. This needs the community to expose client user ids.
- By their Octopus profile id, for example one returned by the community data API.
An id that does not resolve shows the empty state. An id that resolves to the connected user opens their own Activity screen.
- Android
- iOS
- Flutter
- React Native
- Unity
Register octopusComposables() in your NavHost first: see Display the community.
// By client user id
navController.navigateToOctopusActivityByClientUserId(clientUserId = "YOUR_USER_ID")
// By Octopus profile id
navController.navigateToOctopusActivity(userId = octopusUserId)
Pass the activity initial screen to OctopusHomeScreen:
// By client user id
OctopusHomeScreen(
octopus: octopus,
initialScreen: .activity(.init(clientUserId: "YOUR_USER_ID"))
)
// By Octopus profile id
OctopusHomeScreen(
octopus: octopus,
initialScreen: .activity(.init(profileId: octopusProfileId))
)
Pass the activity initial screen to OctopusHomeScreen:
// By client user id
OctopusHomeScreen(
initialScreen: OctopusInitialScreen.activity(
ActivityScreenInfo.clientUserId('YOUR_USER_ID'),
),
)
// By Octopus profile id
OctopusHomeScreen(
initialScreen: OctopusInitialScreen.activity(
ActivityScreenInfo.profileId(octopusProfileId),
),
)
Parameters:
member: exactly one ofclientUserIdorprofileId. Passing both or neither throws anErrorbefore the native call.
import { openUI } from '@octopus-community/react-native';
// By client user id
await openUI({
initialScreen: { type: 'activity', member: { clientUserId: 'YOUR_USER_ID' } },
});
// By Octopus profile id
await openUI({
initialScreen: { type: 'activity', member: { profileId: octopusProfileId } },
});
OpenActivity(memberId, navigationMode) ≥ 1.14.0 opens another member's activity. Parameters:
memberId: OctopusCommunityMemberId: build it withOctopusCommunityMemberId.FromClientUserIdorOctopusCommunityMemberId.FromProfileId. Both throwArgumentExceptionon a null or empty id, and anullOctopusCommunityMemberIdthrowsArgumentNullException.navigationMode: OctopusNavigationMode?: optional iOS navigation container,NavigationStackorAutomatic. Android ignores it.
OpenActivity(navigationMode) ≥ 1.13.0 opens the connected user's activity, or the main feed when no user is connected. A literal OpenActivity(null) calls this form.
// By client user id
OctopusSDK.OpenActivity(OctopusCommunityMemberId.FromClientUserId("YOUR_USER_ID"));
// By Octopus profile id
OctopusSDK.OpenActivity(OctopusCommunityMemberId.FromProfileId(octopusProfileId));
// The connected user
OctopusSDK.OpenActivity(navigationMode: OctopusNavigationMode.Automatic);
Open a member's profile screen directly
From your own UI, for example a member list, open a member's Octopus profile. It is a read-only view of their profile and posts, even when the id is the connected user's. To open the connected user's editable profile, use the entry points in Open a specific screen.
A client user id that does not resolve shows an error state. It never falls back to the connected user's profile.
- Android
- iOS
- Flutter
- React Native
- Unity
Register octopusComposables() in your NavHost first: see Display the community.
// By Octopus profile id
navController.navigateToOctopusProfile(userId = octopusUserId)
// By client user id
navController.navigateToOctopusProfileByClientUserId(clientUserId = "YOUR_USER_ID")
Present OctopusProfileScreen natively, for example in a .fullScreenCover. It has its own navigation container: do not put it in another one. Parameters:
clientUserId: String? = nil: the member to show.nilshows the connected user's own profile. There is no Octopus profile id variant.navigationMode: OctopusNavigationMode = .navigationStack: the navigation container. The default suits a modal presentation.navBarLeadingAction: OctopusNavBarLeadingAction? = nil: a close or back item for places where the SDK cannot dismiss the screen itself, such as your own navigation stack. Pass.close(onTap:)or.back(onTap:): your closure runs on tap.
.fullScreenCover(isPresented: $showProfile) {
OctopusProfileScreen(octopus: octopus, clientUserId: "YOUR_USER_ID")
}
Use the OctopusProfileScreen widget. Without clientUserId, it shows the connected user's own editable profile. There is no Octopus profile id variant: with only a profile id, open the member's activity.
// A widget
OctopusProfileScreen(clientUserId: 'YOUR_USER_ID')
// Or the profile initial screen
OctopusHomeScreen(
initialScreen: OctopusInitialScreen.profile(clientUserId: 'YOUR_USER_ID'),
)
Without clientUserId, or with a blank one, the connected user's own editable profile opens. There is no Octopus profile id variant: with only a profile id, open the member's activity.
import { openUI } from '@octopus-community/react-native';
await openUI({
initialScreen: { type: 'profile', clientUserId: 'YOUR_USER_ID' },
});
Parameters of OpenProfile:
clientUserId: string: optional.nullor blank opens the connected user's profile. There is no Octopus profile id variant.navigationMode: OctopusNavigationMode?: optional iOS navigation container.nullkeeps the default,NavigationStack. Android ignores it.
// The connected user
OctopusSDK.OpenProfile();
// Another member
OctopusSDK.OpenProfile(clientUserId: "YOUR_USER_ID",
navigationMode: OctopusNavigationMode.NavigationStack);
An unknown id shows the SDK unavailable screen.
Read a member's community data
Show a member's community stats in your own profile screen. The SDK gives a read-only snapshot per member, fetched on demand or observed. It holds:
- The member's Octopus profile id.
- Their message count: posts, comments and replies.
nullwhen the community does not show it. - Their gamification standing,
nullwhen gamification is off: alevel, starting at 0, and ascore, alwaysnullfor other members for now.
The client user id variants need the community to expose client user ids. The Octopus id variants always work. Observed values are null while the member is unknown, and update after each fetch.
- Android
- iOS
- Flutter
- React Native
- Unity
The Octopus id variants are fetchCommunityData(userId) and communityDataFlow(userId).
// Fetch once (suspend). null when the member is unknown or the lookup fails.
val data: OctopusCommunityData? =
OctopusSDK.fetchCommunityDataByClientUserId(clientUserId = "YOUR_USER_ID")
// Observe. Emits null while the member is unknown or the lookup fails.
OctopusSDK.communityDataFlowByClientUserId(clientUserId = "YOUR_USER_ID")
.collect { data ->
val messageCount: Int? = data?.messageCount
val level: Int? = data?.gamification?.level
}
The Octopus id variants are the fetchCommunityData(profileId:) and communityDataPublisher(profileId:) overloads.
// Fetch once. nil when the member is unknown; throws on lookup, network or server errors.
let data: OctopusCommunityData? =
try await octopus.fetchCommunityData(clientUserId: "YOUR_USER_ID")
// Observe. The publisher never fails: a failed lookup emits nil.
octopus.communityDataPublisher(clientUserId: "YOUR_USER_ID")
.sink { data in
let messageCount: Int? = data?.messageCount
let level: Int? = data?.gamification?.level
}
Pass exactly one of clientUserId: or profileId: to either call. fetchCommunityData is an instance method, communityDataFlow is static.
// Fetch once. null when the member is unknown. The future fails on a network or
// server error, or when the community does not expose client user ids.
try {
final OctopusCommunityData? data = await OctopusSDK().fetchCommunityData(
clientUserId: 'YOUR_USER_ID',
);
final int? messageCount = data?.messageCount;
final int? level = data?.gamification?.level;
} on ArgumentError {
rethrow; // Both ids, or neither: a programming error
} catch (_) {
// The fetch failed: keep what you already show
}
// Observe. A failed lookup emits null. Single subscription: re-subscribe after switchCommunity.
OctopusSDK.communityDataFlow(clientUserId: 'YOUR_USER_ID').listen((data) {
final int? messageCount = data?.messageCount;
final int? level = data?.gamification?.level;
});
Passing both ids, or neither, raises an ArgumentError. communityDataFlow throws it when you build the stream. fetchCommunityData fails its future with it, so a bare catch (_) would hide it: catch ArgumentError first, as above. The stream also forwards an error when observation cannot start, for example before initialize().
fetchCommunityData and startObservingCommunityData take one object with exactly one of profileId or clientUserId. Passing both or neither throws an Error before the native call. You observe one member at a time: starting a new observation replaces the previous one.
import {
addCommunityDataListener,
fetchCommunityData,
startObservingCommunityData,
stopObservingCommunityData,
} from '@octopus-community/react-native';
// Fetch once. null when the member is unknown; rejects on a network or server error.
const data = await fetchCommunityData({ clientUserId: 'YOUR_USER_ID' });
const messageCount: number | null = data?.messageCount ?? null;
const level: number | null = data?.gamification?.level ?? null;
// Observe
const subscription = addCommunityDataListener((snapshot) => {
const count = snapshot?.messageCount ?? null;
});
await startObservingCommunityData({ profileId: octopusProfileId });
// Later
await stopObservingCommunityData();
subscription.remove();
Call these APIs on the Unity main thread. Callbacks and events arrive on it. Parameters:
memberId: OctopusCommunityMemberId:FromClientUserId, which needs client user ids exposed, orFromProfileId. The factories reject a null or empty id, and anullmemberIdthrowsArgumentNullException.onResult: Action<OctopusCommunityData>: receives a snapshot, ornullwhen unavailable.onError: Action<string>: reports failures, and pending fetches cancelled by a lifecycle change.
You observe one member at a time, and cached data is replayed. The observed member is bound again after initialization or a community switch. Reset and stop end the observation. MessageCount and Gamification can be null, and Gamification.Score is not available.
var memberId = OctopusCommunityMemberId.FromClientUserId("YOUR_USER_ID");
System.Action<OctopusCommunityData> onData = data =>
{
if (data == null) return;
string profileId = data.ProfileId;
int? messageCount = data.MessageCount;
int? level = data.Gamification == null ? (int?)null : data.Gamification.Level;
// A missing value is not zero
};
OctopusSDK.OnCommunityDataChanged += onData;
OctopusSDK.StartObservingCommunityData(memberId);
OctopusSDK.FetchCommunityData(memberId, onData,
message => { /* The fetch failed */ });
// When the screen goes away
OctopusSDK.StopObservingCommunityData();
OctopusSDK.OnCommunityDataChanged -= onData;
The connected user's own client user id is also on their profile object, as clientUserId, next to the fields in Read connected-user entitlements. It is null for guests and for users signed in with Octopus authentication.
Read a member's comments and replies
Android ≥ 1.14.0iOS not availableFlutter not availableReact Native not availableUnity not available
A comments feed holds the comments and replies one member wrote, with their parent content. Follow the community's visibility setting when you show another member's comments in your own UI.
- Android
- iOS
- Flutter
- React Native
- Unity
Show another member's comments only when CommunityConfig.showCommentsOnOtherProfiles is true (default false) and their Profile.commentsFeeds.descendingId is not empty.
After initialization, OctopusSDK.userCommentRepository gives the UserCommentRepository read API. Both methods suspend and return OctopusResult<UserCommentsPage, Nothing>:
firstPage(feedId: String, pageSize: Int):feedIdis the member'sProfile.commentsFeeds.descendingId; skip the call when it is null or empty.pageSizeis the maximum number of entries per page, with no default.nextPage(pageCursor: String, pageSize: Int):pageCursoris the previous page'snextPageCursor. Stop when it is null.
import com.octopuscommunity.sdk.OctopusSDK
import com.octopuscommunity.sdk.domain.model.Profile
import com.octopuscommunity.sdk.domain.model.UserComment
import com.octopuscommunity.sdk.domain.network.OctopusResult
suspend fun readComments(profile: Profile): OctopusResult<List<UserComment>, Nothing> {
val feedId = profile.commentsFeeds.descendingId
?.takeIf { it.isNotEmpty() } ?: return OctopusResult.Success(emptyList())
val repository = OctopusSDK.userCommentRepository
val comments = mutableListOf<UserComment>()
var result = repository.firstPage(feedId = feedId, pageSize = 20)
while (true) {
when (val pageResult = result) {
is OctopusResult.Success -> {
comments.addAll(pageResult.data.comments)
val cursor = pageResult.data.nextPageCursor
?: return OctopusResult.Success(comments)
result = repository.nextPage(pageCursor = cursor, pageSize = 20)
}
is OctopusResult.Failure -> {
// Let the caller show an error or a retry
return pageResult
}
}
}
}
UserCommentsPage.comments lists the most recent first. Each UserComment has id, updateDate, content, a non-null post, and parentComment, non-null for replies. content is UserComment.Content.OfComment (with comment) or UserComment.Content.OfReply (with reply). Use nextPageCursor, not the page length, to decide whether to load more. Show a failure as an error, never as an empty feed, and in a scrolling UI load one page at a time.
Reading a member's comments is not yet available on iOS. The SDK Comments tab shows them.
Reading a member's comments is not yet available on Flutter.
Reading a member's comments is not yet available on React Native.
Reading a member's comments is not yet available on Unity.
Behavior and limits
- Unified Profile needs both the community setting and your callback. Without either, the SDK keeps its own profile screens.
- The profile tap callback always gets a client user id. Members without one open the SDK member activity screen.