Navigation and links
Take your users straight to a community screen from anywhere in your app, and decide which links the community opens itself.
Before you begin
- Display the community in your app: see Display the community.
- To open a group, you need its id: see List available groups.
How it works
By default, the community opens on its main feed. You can open it on another screen instead, in one of two ways:
| Mode | What the user sees | Back or close returns to |
|---|---|---|
| Community context | The screen, inside the full community navigation | The community main feed |
| Isolation | The screen alone, without the main feed behind it | Your app |
Community context is available on Android, where the community screens live in your own navigation graph. Every platform can open a screen in isolation.
Open a specific screen
Each platform has one entry point per mode. The sections below then show each screen in the same order: a post, a group, a member, and the post editor.
Open in the community context
Android availableiOS not availableFlutter not availableReact Native not availableUnity not available
The screen is pushed on your navigation stack, on top of the community graph. The user can navigate back to the main feed.
- Android
- iOS
- Flutter
- React Native
- Unity
Register the community graph with octopusComposables in your NavHost (see Display the community), then call a navigateToOctopus… extension on your NavHostController:
| Screen | Call |
|---|---|
| Main feed | navigateToOctopusHome() |
| Post | navigateToOctopusPost(postId) |
| Group | navigateToOctopusGroup(groupId) |
| Member activity | navigateToOctopusActivity(userId) or navigateToOctopusActivityByClientUserId(clientUserId) |
| Member profile | navigateToOctopusProfile(userId) or navigateToOctopusProfileByClientUserId(clientUserId) |
| Post editor | navigateToOctopusCreatePost(info) |
navController.navigateToOctopusPost(postId = "YOUR_POST_ID")
Opening a screen in the community context is not yet available on iOS. Open it in isolation instead.
Opening a screen in the community context is not yet available on Flutter. Open it in isolation instead.
Opening a screen in the community context is not yet available on React Native. Open it in isolation instead.
Opening a screen in the community context is not yet available on Unity. Open it in isolation instead.
Open a screen in isolation
The screen opens alone. Its back arrow, close button or system back gesture returns the user to your app.
- Android
- iOS
- Flutter
- React Native
- Unity
Show a dedicated screen composable. It does not need the community graph.
| Screen | Composable |
|---|---|
| Post | OctopusPostDetailsScreen(navController, postId) |
| Group | OctopusGroupDetailsScreen(navController, groupId) |
| Post editor | OctopusCreatePostScreen(navController, info) |
OctopusPostDetailsContent and OctopusGroupDetailsContent are the same screens with navigation callbacks: login, profile edit, URL handling and profile taps. Use them when your users sign in with your own account system.
Pass initialScreen to OctopusHomeScreen:
| Screen | initialScreen |
|---|---|
| Main feed (default) | .mainFeed |
| Post | .post(.init(postId:)) |
| Group | .group(.init(groupId:)) |
| Member activity | .activity(.init(clientUserId:)) or .activity(.init(profileId:)) |
| Post editor | .createPost(.init(prefilledPost:)) |
A member profile opens with OctopusProfileScreen: see Open a member's profile screen directly.
Pass an OctopusInitialScreen to the initialScreen parameter of OctopusHomeScreen:
| Screen | initialScreen |
|---|---|
| Main feed (default) | OctopusInitialScreen.mainFeed() |
| Post | OctopusInitialScreen.post(PostScreenInfo(postId:)) |
| Group | OctopusInitialScreen.group(GroupScreenInfo(groupId:)) |
| Member activity ≥ 1.13.1 | OctopusInitialScreen.activity(ActivityScreenInfo.clientUserId(…)) or ActivityScreenInfo.profileId(…) |
| Member profile ≥ 1.13.1 | OctopusInitialScreen.profile(clientUserId:) |
| Post editor | OctopusInitialScreen.createPost(CreatePostScreenInfo(…)) |
OctopusPostDetailsScreen(postId:) and OctopusGroupDetailsScreen(groupId:) are shortcuts for a post or a group. They take the same callbacks as OctopusHomeScreen.
When you also pass a notification that carries a deep link, the deep link wins and initialScreen is ignored.
Pass initialScreen to openUI(), or as a prop of <OctopusUIView>:
| Screen | initialScreen |
|---|---|
| Main feed (default) | { type: 'mainFeed' } |
| Post | { type: 'post', postId } |
| Group | { type: 'group', groupId } |
| Member activity | { type: 'activity', member } |
| Member profile | { type: 'profile', clientUserId? } |
| Post editor | { type: 'createPost', prefilledPost? } |
<OctopusUIView> reads initialScreen on mount only: change its key to open another screen. When you also pass a notification, the notification wins and initialScreen is dropped with a warning.
Call an Open… method:
| Screen | Method |
|---|---|
| Main feed | Open() |
| Post | OpenPost(postId) |
| Group | OpenGroup(groupId) |
| Member activity ≥ 1.13.0 | OpenActivity() for the connected user, OpenActivity(memberId) ≥ 1.14.0 for another member |
| Member profile ≥ 1.13.0 | OpenProfile(clientUserId) |
| Post editor | OpenCreatePost(prefilled) |
Each method also takes an optional navigationMode, used on iOS only.
Open a post
- Android
- iOS
- Flutter
- React Native
- Unity
// In the community context
navController.navigateToOctopusPost(postId = "YOUR_POST_ID")
// In isolation, as a destination of your own NavHost
OctopusPostDetailsScreen(
navController = navController,
postId = "YOUR_POST_ID",
)
OctopusHomeScreen(
octopus: octopus,
initialScreen: .post(.init(postId: "YOUR_POST_ID"))
)
OctopusHomeScreen(
initialScreen: OctopusInitialScreen.post(
PostScreenInfo(postId: 'YOUR_POST_ID'),
),
)
import { openUI } from '@octopus-community/react-native';
await openUI({ initialScreen: { type: 'post', postId: 'YOUR_POST_ID' } });
OctopusSDK.OpenPost("YOUR_POST_ID");
An empty or null post id opens the main feed.
Open a group
- Android
- iOS
- Flutter
- React Native
- Unity
// In the community context
navController.navigateToOctopusGroup(groupId = "YOUR_GROUP_ID")
// In isolation, as a destination of your own NavHost
OctopusGroupDetailsScreen(
navController = navController,
groupId = "YOUR_GROUP_ID",
)
OctopusHomeScreen(
octopus: octopus,
initialScreen: .group(.init(groupId: "YOUR_GROUP_ID"))
)
OctopusHomeScreen(
initialScreen: OctopusInitialScreen.group(
GroupScreenInfo(groupId: 'YOUR_GROUP_ID'),
),
)
await openUI({ initialScreen: { type: 'group', groupId: 'YOUR_GROUP_ID' } });
OctopusSDK.OpenGroup("YOUR_GROUP_ID");
Open a member's activity or profile
A member's activity screen lists their posts, and their comments when the community exposes them. Identify the member with your own user id or with their Octopus profile id. The full options are in Open a member's activity screen directly and Open a member's profile screen directly.
- Android
- iOS
- Flutter
- React Native
- Unity
navController.navigateToOctopusActivityByClientUserId(clientUserId = "YOUR_USER_ID")
OctopusHomeScreen(
octopus: octopus,
initialScreen: .activity(.init(clientUserId: "YOUR_USER_ID"))
)
OctopusHomeScreen(
initialScreen: OctopusInitialScreen.activity(
ActivityScreenInfo.clientUserId('YOUR_USER_ID'),
),
)
See Open a member's activity screen directly for the shape of member.
OctopusSDK.OpenActivity(OctopusCommunityMemberId.FromClientUserId("YOUR_USER_ID"));
Open the post editor
The post editor can open empty, or prefilled with content from your app: text, an image, a target group and a call-to-action (CTA). This is how a Bridge Share starts. Every field is optional. The user can edit the text, the image and the group before publishing. The CTA is not shown in the editor: it is attached to the published post.
Rules shared by every platform:
- Text, when present, is 10 to 5000 characters long.
- The image is local data. The SDK never downloads a remote URL.
- A missing or inaccessible group makes the user pick one before publishing.
- A CTA needs both a URL and a label.
To publish a prefilled image in a community that forbids member pictures, sign it on your backend: see Bridge share image signing.
- Android
- iOS
- Flutter
- React Native
- Unity
Parameters of OctopusPrefilledPost:
text(String?): initial text.image(Uri?): local content URI of an image. If the URI comes fromPickVisualMedia, callcontentResolver.takePersistableUriPermission(...)first, so it survives process death.topicId(String?): id of the target group.cta(OctopusPostCTA?): the call-to-action.
Blank values become null. The constructor throws an OctopusPrefilledPost.ValidationError, an IllegalArgumentException: TextTooShort, TextTooLong, CtaLabelEmpty or CtaUrlEmpty. The editor checks the image dimensions when it opens.
import androidx.core.net.toUri
import com.octopuscommunity.sdk.domain.model.CreatePostScreenInfo
import com.octopuscommunity.sdk.domain.model.OctopusPostCTA
import com.octopuscommunity.sdk.domain.model.OctopusPrefilledPost
import com.octopuscommunity.sdk.ui.navigateToOctopusCreatePost
try {
val prefill = OctopusPrefilledPost(
text = "The perfect Canelés",
image = recipeImageUri,
topicId = recipeGroupId, // null lets the user pick a group
cta = OctopusPostCTA(
url = "https://www.example.com/recipes/caneles".toUri(),
label = "Read the recipe",
),
)
navController.navigateToOctopusCreatePost(
info = CreatePostScreenInfo(prefilledPost = prefill),
)
} catch (e: OctopusPrefilledPost.ValidationError) {
// Show your own error; the editor did not open
}
In isolation, pass the same info to OctopusCreatePostScreen(navController, info). CreatePostScreenInfo() opens an empty editor.
Parameters of OctopusPrefilledPost:
text: String?: initial text.image: Data?: image bytes, for example fromjpegData(compressionQuality:).topicId: String?: id of the target group.cta: OctopusPrefilledPost.CTA?: the call-to-action. Its initializer throws too.sign: optional closure that signs a prefilled image. See Bridge share image signing.
The initializer applies the editor rules and throws an OctopusPrefilledPost.ValidationError. Read debugDescription to diagnose it.
import Octopus
import OctopusUI
let prefill: OctopusPrefilledPost
do {
prefill = try OctopusPrefilledPost(
text: "The perfect Canelés",
image: recipeImage.jpegData(compressionQuality: 1),
topicId: recipeGroupId, // nil lets the user pick a group
cta: try .init(
url: URL(string: "https://www.example.com/recipes/caneles")!,
label: "Read the recipe"
)
)
} catch let error as OctopusPrefilledPost.ValidationError {
switch error {
case .textTooShort(let min): print("Text needs \(min) characters or more")
case .textTooLong(let max): print("Text allows \(max) characters at most")
case .imageInvalid, .imageTooSmall, .imageRatioTooLarge: print("Pick another image")
case .ctaLabelEmpty, .ctaUrlEmpty: print("The CTA needs a label and a URL")
case .contentEmpty: print("Nothing to prefill")
@unknown default: print(error.debugDescription)
}
return
} catch {
return
}
// Then show the editor, for example inside a .fullScreenCover
OctopusHomeScreen(
octopus: octopus,
initialScreen: .createPost(.init(prefilledPost: prefill))
)
.createPost(.init()) opens an empty editor. On publish or cancel, the SDK dismisses OctopusHomeScreen and returns to your app.
Parameters of OctopusPrefilledPost:
text(String?): initial text.image(Uint8List?): image bytes. OnlyshowOctopusCreatePostScreencarries them.topicId(String?): id of the target group.cta(OctopusPostCTA?): the call-to-action,OctopusPostCTA(url:, label:).
Empty values become null. The constructor throws an OctopusPrefilledPostValidationError, an ArgumentError.
try {
final prefill = OctopusPrefilledPost(
text: 'The perfect Canelés',
image: recipeImageBytes,
topicId: recipeGroupId, // null lets the user pick a group
cta: OctopusPostCTA(
url: Uri.parse('https://www.example.com/recipes/caneles'),
label: 'Read the recipe',
),
);
await octopus.showOctopusCreatePostScreen(
info: CreatePostScreenInfo(prefilledPost: prefill),
);
} on OctopusPrefilledPostValidationError {
// Show your own error; the editor did not open
}
OctopusInitialScreen.createPost(CreatePostScreenInfo(prefilledPost: prefill)) also opens the editor, embedded in OctopusHomeScreen, but it drops the image on both platforms. Use showOctopusCreatePostScreen to share an image.
Fields of prefilledPost:
text(string): initial text.imageUri(string): afile://URI or a bundled asset name.topicId(string): id of the target group.cta({ url: string; label: string }): the call-to-action.
import {
isNavigateToOctopusCreatePostError,
navigateToOctopusCreatePost,
} from '@octopus-community/react-native';
try {
await navigateToOctopusCreatePost({
text: 'The perfect Canelés',
imageUri: recipeImageUri,
topicId: recipeGroupId, // omit to let the user pick a group
cta: { url: 'https://www.example.com/recipes/caneles', label: 'Read the recipe' },
});
} catch (error) {
if (isNavigateToOctopusCreatePostError(error)) {
// error.code: TEXT_TOO_SHORT, TEXT_TOO_LONG, CTA_LABEL_EMPTY, CTA_URL_EMPTY,
// IMAGE_INVALID, IMAGE_TOO_SMALL or IMAGE_RATIO_TOO_LARGE (image codes on iOS only)
}
}
You can pass the same object as initialScreen: { type: 'createPost', prefilledPost }. <OctopusUIView> cannot reject: an invalid prefill opens an empty editor and logs a native warning.
Fields of OctopusPrefilledPost:
Text(string): initial text.TopicId(string): id of the target group.ImagePath(string): absolute local path of an image, for example underApplication.persistentDataPath.CtaLabel(string) ≥ 1.12.1: label of the CTA button.CtaUrl(string) ≥ 1.12.1: URL the CTA opens.
// Empty editor
OctopusSDK.OpenCreatePost();
// Prefilled editor with a CTA
OctopusSDK.OpenCreatePost(new OctopusPrefilledPost
{
Text = "The perfect Canelés",
TopicId = recipeGroupId,
ImagePath = Application.persistentDataPath + "/caneles.png",
CtaLabel = "Read the recipe",
CtaUrl = "https://www.example.com/recipes/caneles"
});
Unity does not raise validation errors. Empty fields are dropped, and a CTA with only a label or only a URL is dropped.
Intercept URL openings
Members open links from post, comment and reply content, and from CTA buttons. By default, the SDK opens them itself. Intercept them to open some links in your own app instead, for example a page of your website that your app also shows.
Your handler receives the URL and returns a strategy:
| Strategy | Effect |
|---|---|
| Handled by app | Your app handled the URL. The SDK does nothing more. |
| Handled by Octopus | The SDK opens the URL. |
- Android
- iOS
- Flutter
- React Native
- Unity
Pass onNavigateToUrl to octopusComposables, and to OctopusHomeContent or the …Content screens you use.
import androidx.core.net.toUri
import com.octopuscommunity.sdk.ui.UrlOpeningStrategy
octopusComposables(
navController = navController,
onNavigateToUrl = { url ->
val uri = url.toUri()
if (uri.host == "www.example.com" && uri.path == "/contact") {
navController.navigate(ContactRoute)
UrlOpeningStrategy.HandledByApp
} else {
UrlOpeningStrategy.HandledByOctopus
}
},
)
See the UrlHandler of the Android sample.
≥ 1.13.2 With HandledByOctopus, the SDK opens the link in a Chrome Custom Tab colored from the SDK palette, including the one you install through the theming container of octopusComposables. The tab takes light or dark from that palette when it opens, not from the device setting: an open tab does not follow a system dark mode change. Transparent colors are not forwarded to Chrome, which uses its own default for them.
octopus.set(onNavigateToURLCallback: { url in
if url.host == "www.example.com" && url.path == "/contact" {
// Open the contact page inside your app
return .handledByApp
}
return .handledByOctopus
})
See the URLManager of the iOS sample.
OctopusHomeScreen(
onNavigateToUrl: (url) {
final uri = Uri.parse(url);
if (uri.host == 'www.example.com' && uri.path == '/contact') {
// Open the contact page inside your app
return UrlOpeningStrategy.handledByApp;
}
return UrlOpeningStrategy.handledByOctopus;
},
)
Register a listener, then enable interception with interceptUrls: true on openUI(), or with the interceptUrls prop of <OctopusUIView>. Register the listener before the view mounts.
import {
addNavigateToUrlListener,
openUI,
UrlOpeningStrategy,
} from '@octopus-community/react-native';
const subscription = addNavigateToUrlListener(async (url) => {
const parsed = new URL(url);
if (parsed.hostname === 'www.example.com' && parsed.pathname === '/contact') {
// Open the contact page inside your app
return UrlOpeningStrategy.handledByApp;
}
return UrlOpeningStrategy.handledByOctopus;
});
await openUI({ interceptUrls: true });
// When you no longer need it
subscription.remove();
Assign OctopusSDK.NavigateToUrlHandler, a Func<string, UrlOpeningStrategy>. It is a single slot, not an event: if several components need URLs, forward them from one central handler. Without a handler, the SDK opens every URL.
UrlOpeningStrategy.HandledByApp: your app handled the URL. Only this value brings the player back to the foreground.UrlOpeningStrategy.HandledByOctopus: the SDK opens the URL in the system browser and keeps the community screen open.
using UnityEngine;
public class UrlInterceptor : MonoBehaviour
{
void Start()
{
OctopusSDK.NavigateToUrlHandler = OnNavigateToUrl;
}
void OnDestroy()
{
// The slot is shared: clear it only if it is still ours
if (OctopusSDK.NavigateToUrlHandler == OnNavigateToUrl)
OctopusSDK.NavigateToUrlHandler = null;
}
// Runs on a background thread: keep it fast, no Unity API calls
UrlOpeningStrategy OnNavigateToUrl(string url)
{
if (!string.IsNullOrEmpty(url) && url.StartsWith("mygame://"))
{
return UrlOpeningStrategy.HandledByApp;
}
return UrlOpeningStrategy.HandledByOctopus;
}
}
≥ 1.12.2 On iOS and Android, the handler runs synchronously on a background thread, even while the player loop is paused. Keep it to a fast, thread-safe decision, and do the Unity work once your app regains focus.