# social.plus SDK > social.plus is a feature-rich SDK for building chat, communities, feeds, live video, and social experiences. This document provides AI-consumable documentation for the full SDK: Getting Started, Core Concepts, Video SDK, Chat SDK, and Social SDK. ## Getting Started ### [Installation & Quick Start](https://learn.social.plus/social-plus-sdk/getting-started/overview) > Install social.plus SDK and start building social features in minutes. Follow our straightforward setup guide. Install the social.plus SDK and add social features to your app, whether you're building for mobile, web, or cross-platform. This guide covers setup and your first integration. **New to social.plus SDK?** Check out our [SDK Overview](/social-plus-sdk/overview) to learn about all available features and choose what's right for your project. ## Parameters | Operation | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | --- | | Initialize SDK | API key | Yes | iOS, Android, TypeScript, Flutter | Application API key from the social.plus Console. | | Initialize SDK | Region / endpoint | Yes | iOS, Android, TypeScript, Flutter | Region where your social.plus application was created, such as US, EU, or SG. | | Authenticate user | `userId` | Yes | iOS, Android, TypeScript, Flutter | Stable user identifier from your own identity system. | | Authenticate user | `displayName` | No | iOS, Android, TypeScript, Flutter | Display name stored in the social.plus user profile. | | Authenticate user | `authToken` | Production | iOS, Android, TypeScript, Flutter | Backend-generated auth token for production authentication. | | Authenticate user | `sessionHandler` | Recommended | iOS, Android, TypeScript, Flutter | Token-renewal handler used by production session flows. | Select the SDK that matches your development environment: - [iOS SDK](/social-plus-sdk/getting-started/platform-setup/mobile/ios-quick-start) - [Android SDK](/social-plus-sdk/getting-started/platform-setup/mobile/android-quick-start) - [Flutter SDK](/social-plus-sdk/getting-started/platform-setup/mobile/flutter-quick-start) - [TypeScript SDK](/social-plus-sdk/getting-started/platform-setup/web/web-quick-start) 1. Visit the social.plus Admin Console 2. Navigate to **Settings** → **Integrations** 3. Copy your API key from the **API Key** section Keep production API keys in environment or build configuration instead of hardcoding them in public source. Backend auth tokens and secrets must stay server-side. 1. Install the SDK using the [platform-specific instructions](https://learn.social.plus/social-plus-sdk/getting-started/platform-setup/mobile/ios-quick-start) 2. Initialize the SDK with your API key and the region where your social.plus application was created (US, EU, or SG) ```swift iOS let client = try! AmityClient(apiKey: "your-api-key", region: .SG) ``` ```kotlin Android class ChatApp : Application() { override fun onCreate() { super.onCreate() AmityCoreClient.setup( apiKey = "your-api-key", endpoint = AmityEndpoint.EU // optional param, defaulted as SG region ) } } ``` ```typescript TypeScript import { Client } from '@amityco/ts-sdk'; const client = Client.createClient('your-api-key', 'sg'); ``` ```dart Flutter void setup() async { await AmityCoreClient.setup( option: AmityCoreClientOption.create( apiKey: 'your-api-key', endpoint: AmityEndpoint.SG ), sycInitialization: true); } ``` Log in users to access social.plus features: The examples pass a `sessionHandler`; see the [Authentication guide](/social-plus-sdk/getting-started/authentication#session-handlers) for production token renewal patterns. ```swift iOS Task { @MainActor in do { try await client.login( userId: "", displayName: "<(optional)-display-name>", authToken: "<(optional)-auth-token>", sessionHandler: sessionHandler ) print("login success") } catch { print("login failed \(error)") } } ``` ```kotlin Android fun authenticate() { AmityCoreClient.login(userId = "userId 1", sessionHandler = sessionHandler) .displayName(displayName = "John Doe") // optional .authToken(authToken = "token") // optional .build() .submit() .doOnComplete { //success } .subscribe() } ``` ```typescript TypeScript const { Client } = await import('@amityco/ts-sdk'); const sessionHandler: Amity.SessionHandler = { sessionWillRenewAccessToken(renewal) { renewal.renew(); }, }; const handleConnect = async (userId: string, displayName: string) => { /* * NOTE: * client instance must be created prior to logging in * * createClient also accepts an optional object to specify further details * such as debugSession & apiEndpoint * ex: * { * debugSession?: 'string', * apiEndpoint?: { http?: 'http.endpoint', mqtt?: 'mqtt.endpoint' } * } */ const client = Client.createClient('your-api-key', 'sg'); await Client.login({ userId, displayName }, sessionHandler); }; handleConnect('userId', 'Bob Newton'); ``` ```dart Flutter void login() async { await AmityCoreClient.login('userId', sessionHandler: (AccessTokenRenewal renewal) { renewal.renew(); }) .displayName('userDisplayName') .submit(); } ``` ## What's Next? Once you have the SDK installed and initialized, you can start building: Learn about session management and secure authentication flows Create communities, posts, and social feeds Build real-time messaging and channels Add live streaming and video features ## Platform Requirements Make sure your development environment meets these minimum requirements: - Xcode 26.0+ - iOS 14.0+ - Swift 5.0+ - Android 6.0 (API 23)+ - Target SDK 36+ - Compile SDK 36+ - Kotlin 2.2.0+ - Chrome 38+ - Firefox 42+ - Safari 9+ - Edge 13+ - Opera: 25+ - React Native 0.60+ - Node.js 14+ - iOS 14.0+ / Android 6.0 (API 23)+ - Flutter 3.0.0 - 4.0.0 - Dart SDK >=3.0.0 <4.0.0 - iOS 12.0+ / Android 4.4 (API 19)+ ## Need Help? Get help from our community Explore detailed guides and API references --- ### [Authentication](https://learn.social.plus/social-plus-sdk/getting-started/authentication) > Authentication is required to access social.plus features. This comprehensive guide will take you from basic login to production-ready authentication patterns. ## Overview & Concepts ### Authentication in social.plus social.plus uses a **dual authentication approach** to ensure both application security and user verification: **Purpose**: Identifies your application to social.plus servers\ **Scope**: Application-level authentication\ **Usage**: Required for SDK initialization\ **Security**: Store in app configuration; do not hardcode in public source **Purpose**: Server-to-server verification that the user is validated by your backend\ **Scope**: User-level authentication with your system\ **Usage**: Optional for development, required for production\ **Security**: Generated by your backend, proves user is legitimate ### How Auth Tokens Work Auth tokens enable **server-to-server communication** between social.plus and your backend: Your app authenticates the user with your own authentication system (login, OAuth, etc.) Your backend server generates an auth token for the verified user and sends it to your app. [Learn how to implement this →](/social-plus-sdk/getting-started/authentication#backend-token-generation) When your app logs into social.plus, the auth token proves to social.plus that your backend has verified this user social.plus can now trust that this user session is legitimate and validated by your system ```mermaid sequenceDiagram participant User as User Device participant App as Your App participant Backend as Your Backend participant Social as social.plus SDK participant SocialAPI as social.plus API User->>App: Login Request App->>Backend: Authenticate User Backend->>Backend: Verify Credentials Backend->>Backend: Generate Auth Token Backend->>App: Return Auth Token App->>Social: login(userId, authToken) Social->>SocialAPI: Verify Auth Token SocialAPI->>SocialAPI: Validate Token Signature SocialAPI->>Social: Authentication Success Social->>App: Login Complete App->>User: Access Granted ``` **Why Auth Tokens?** This approach ensures that only users who have been properly authenticated by your backend can access social.plus features, maintaining security and preventing unauthorized access. ### When Do You Need Each? ```typescript // Production mode - API key + auth tokens const client = Client.createClient('your-prod-api-key', 'sg'); // Secure login with auth token from your backend await Client.login({ userId: 'user-123', displayName: 'John Doe', authToken: 'token-generated-by-your-backend' // Proves user is verified by your system }, sessionHandler); ``` Auth tokens must be generated by your secure backend after verifying the user. This ensures server-to-server trust between social.plus and your authentication system. ```typescript // Development mode - API key only (for testing) const client = Client.createClient('your-dev-api-key', 'sg'); // Simple login without auth token (development/testing only) await Client.login({ userId: 'dev-user-123', displayName: 'Developer' }, sessionHandler); ``` Development mode bypasses auth token requirements for easier testing. Never use this in production as it skips your backend verification. ## Parameters | Operation | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | --- | | Initialize SDK | API key | Yes | iOS, Android, TypeScript, Flutter | Application API key from the social.plus Console. | | Initialize SDK | Region / endpoint | Yes | iOS, Android, TypeScript, Flutter | Region where your social.plus application was created. | | Login user | `userId` | Yes | iOS, Android, TypeScript, Flutter | Stable user identifier from your identity system. | | Login user | `displayName` | No | iOS, Android, TypeScript, Flutter | Display name stored in the social.plus user profile. | | Login user | `authToken` | Production | iOS, Android, TypeScript, Flutter | Backend-generated token proving your system authenticated the user. | | Login user | `sessionHandler` | Recommended | iOS, Android, TypeScript, Flutter | Callback used to renew auth tokens when the SDK session expires. | | Login with access token | `accessToken` | Yes | iOS, Android, TypeScript | JWT access token issued by your backend. | | Login with access token | access token handler | Yes | iOS, Android, TypeScript | Handler registered before login so the SDK can renew access tokens. | | Logout | None | No | iOS, Android, TypeScript, Flutter | Ends the current SDK session; secure logout revokes the access token where supported. | ## Quick Start (Basic Authentication) ### Step 1: Initialize the SDK Start by setting up the social.plus client with your API key: ```swift iOS let client = try! AmityClient(apiKey: "your-api-key", region: .SG) ``` ```kotlin Android AmityCoreClient.setup( apiKey = "your-api-key", endpoint = AmityEndpoint.SG ) ``` ```typescript TypeScript import { Client } from '@amityco/ts-sdk'; const client = Client.createClient('your-api-key', 'sg'); ``` ```dart Flutter await AmityCoreClient.setup( option: AmityCoreClientOption.create( apiKey: 'your-api-key', endpoint: AmityEndpoint.SG ) ); ``` ### Step 2: Login User Authenticate users to access social.plus features: The examples pass a `sessionHandler`; see [Session Handlers](#session-handlers) for production token renewal patterns. ```swift iOS Task { @MainActor in do { try await client.login( userId: "user-123", displayName: "John Doe", authToken: "your-auth-token", // Optional for development sessionHandler: sessionHandler ) print("Login successful") } catch { print("Login failed: \(error)") } } ``` ```kotlin Android AmityCoreClient.login(userId = "user-123", sessionHandler = sessionHandler) .displayName(displayName = "John Doe") .authToken(authToken) // Optional for development .build() .submit() .doOnComplete { // Login successful } .doOnError { error -> // Login failed } .subscribe() ``` ```typescript TypeScript try { await Client.login({ userId: 'user-123', displayName: 'John Doe', authToken: 'your-auth-token', // Optional for development }, sessionHandler); console.log('Login successful'); } catch (error) { console.error('Login failed:', error); } ``` ```dart Flutter try { await AmityCoreClient.login('user-123') .displayName('John Doe') .authToken('your-auth-token') // Optional for development .submit(); print('Login successful'); } catch (error) { print('Login failed: $error'); } ``` ### Step 3: Check Authentication Status Verify if a user is currently logged in: ```swift iOS if case .established = client.sessionState { let currentUserId = client.currentUserId print("Current user: \(currentUserId ?? "")") } else { // Proceed to social.plus Authentication // authenticateToSocialPlus() } ``` ```kotlin Android if (AmityCoreClient.getCurrentSessionState() == SessionState.Established) { val currentUserId = AmityCoreClient.getUserId() Log.d("Auth", "Current user: $currentUserId") } else { // Proceed to social.plus Authentication authenticateToSocialPlus() } ``` ```typescript TypeScript // `client` is the instance returned by Client.createClient() during initialization if (client.sessionState === Amity.SessionStates.ESTABLISHED) { const currentUserId = client.userId; console.log('Current user:', currentUserId); } else { // Proceed to social.plus Authentication authenticateToSocialPlus(); } ``` ```dart Flutter final sessionState = await AmityCoreClient.observeSessionState().first; if (sessionState == SessionState.Established) { final currentUserId = AmityCoreClient.getUserId(); print('Current user: $currentUserId'); } else { // Proceed to social.plus Authentication authenticateToSocialPlus(); } ``` ### Step 4: Logout End the user session: For an extra layer of security, which ensures accessToken revocation prior to performing logout(). Should the SDK fail to revoke the accessToken, the SDK will not proceed to logout and will throw an exception to notify the failure. ```swift iOS do { try await client.secureLogout() } catch { /// Handle error from revoking accessToken here } ``` ```kotlin Android AmityCoreClient.secureLogout() .doOnComplete { // Void } .doOnError { // Exception } .subscribe() ``` ```typescript TypeScript const handleSecureLogout = async () => { await Client.secureLogout(); }; handleSecureLogout(); ``` ```dart Flutter // Flutter SDK does not expose secureLogout() — use logout() instead. // secureLogout (with access token revocation) is an iOS/Android/TS feature. await AmityCoreClient.logout(); ``` ## Understanding Session States Session states indicate what's happening with user authentication. social.plus SDK automatically manages these states according to the flow shown in the diagram below: **Ready for login** - user needs to authenticate\ _Entry points_: App start (no session), logout, login failure **Login in progress** - authentication being processed\ _Entry point_: When login() is called from notLoggedIn state **Fully authenticated** - SDK ready, all features available\ _Entry points_: Successful login, successful token renewal **Token renewal needed** - automatic renewal attempted\ _Entry point_: When auth token expires during established state **Session forcibly ended** - user banned or deleted\ _Entry points_: User banned/deleted from established or tokenExpired states ### Session Flow Understanding the complete session state flow helps you build responsive apps: **App starts**: Always begins in the `start` state, then immediately moves to: - **No saved session**: Moves to `notLoggedIn` state - **Has saved session**: Moves to `established` state (if session valid) **User attempts login**: - `notLoggedIn` → `establishing` (login in progress) - **Login succeeds**: `establishing` → `established` - **Login fails**: `establishing` → `notLoggedIn` **`During active use in established state`**: - **Token expires**: `established` → `tokenExpired` - **Token renewed successfully**: `tokenExpired` → `established` - **User banned/deleted**: `established` → `terminated` - **Manual logout**: `established` → `notLoggedIn` **`When in tokenExpired state`**: - **Auto-renewal succeeds**: Returns to `established` state - **Auto-renewal fails**: User may need to re-authenticate - **User banned during renewal**: Moves to `terminated` state - **Manual logout**: Moves to `notLoggedIn` state **`From terminated state`**: - Only way out is through logout → `notLoggedIn` - User must re-authenticate to access features again Session state diagram image temporarily removed while the asset is unavailable. The textual flow description above preserves the full logic. ### Observing Session State Monitor session state changes to handle authentication in your app: {/* doc-as-test: skip (truly-illustrative: UIViewController scaffold — 'self' only valid inside a class body, not in a free function) */} ```swift var cancellable: AnyCancellable? // Observe session state changes cancellable = client.$sessionState.sink { sessionState in switch sessionState { case .notLoggedIn: // Show login screen self?.showLogin() case .establishing: // Show loading indicator self?.showLoading() case .established: // Hide loading indicator, proceed to app self?.hideLoading() self?.proceedToApp() case .tokenExpired: // Attempt to refresh token (Optional) self?.showTokenRefreshIndicator() case .terminated: // Handle session termination self?.handleTermination() } } ``` ```kotlin // Observe session state AmityCoreClient.observeSessionState() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnNext { sessionState: SessionState -> when (sessionState) { is SessionState.NotLoggedIn -> { // Show login screen showLogin() } is SessionState.Establishing -> { // Show loading indicator } is SessionState.Established -> { // Hide loading indicator, proceed to app } is SessionState.TokenExpired -> { // Attempt to refresh token (Optional) } is SessionState.Terminated -> { // Handle session termination } else -> {} } } .doOnError { // Exception } .subscribe() ``` ```typescript // Listen to session state changes import { Client } from '@amityco/ts-sdk'; const unsubscribe = Client.onSessionStateChange((state: Amity.SessionStates) => { switch (state) { case 'notLoggedIn': showLoginForm(); break; case 'establishing': showLoadingSpinner(); break; case 'established': hideLoadingSpinner(); navigateToApp(); break; case 'tokenExpired': showTokenRefreshIndicator(); break; case 'terminated': handleSessionTermination(); break; } }); // Call unsubscribe() when the screen no longer needs session updates. ``` ```dart // Listen to session state changes AmityCoreClient.observeSessionState().listen((state) { switch (state) { case SessionState.NotLoggedIn: // Show login screen showLoginScreen(); break; case SessionState.Establishing: // Show loading indicator showLoadingIndicator(); break; case SessionState.Established: // Hide loading indicator, proceed to app hideLoadingIndicator(); proceedToApp(); break; case SessionState.TokenExpired: // Attempt to refresh token (Optional) showTokenRefreshIndicator(); break; case Terminated(): // Handle session termination handleSessionTermination(); } }); ``` ## Advanced Session Management For production apps, you'll need sophisticated session handling with automatic token refresh: ### Session Handlers for Token Refresh Session handlers automatically manage token lifecycle: ```swift class ProductionSessionHandler: SessionHandler { func sessionWillRenewAccessToken(renewal: AccessTokenRenewal) { // Call your backend to get a fresh token Task { do { // let newToken = try await YourAuthService.fetchToken() // renewal.renewWithAuthToken(authToken: newToken) renewal.unableToRetrieveAuthToken() // replace with real token fetch } } } } // Use during login let userId = "user-id" let displayName: String? = "Display Name" let authToken: String? = nil let sessionHandler = ProductionSessionHandler() try await client.login( userId: userId, displayName: displayName, authToken: authToken, sessionHandler: sessionHandler ) ``` ```kotlin class ProductionSessionHandler : SessionHandler { override fun sessionWillRenewAccessToken(renewal: AccessTokenRenewal) { // Call your backend to refresh token; renewal.renewWithAuthToken(newToken) // or renewal.unableToRetrieveAuthToken() on failure } } // Use during login val productionSessionHandler = ProductionSessionHandler() AmityCoreClient.login(userId = userId, sessionHandler = productionSessionHandler) .displayName(displayName) .authToken(authToken) .build() .submit() ``` ```typescript interface SessionHandler { sessionWillRenewAccessToken: (renewal: TokenRenewal) => void; } const createProductionSessionHandler = (): Amity.SessionHandler => ({ sessionWillRenewAccessToken: async (renewal: Amity.AccessTokenRenewal) => { try { // Request fresh token from your backend (server-to-server) // Your backend re-verifies the user and generates new token const newToken = await AuthService.refreshToken(); renewal.renewWithAuthToken(newToken); } catch (error) { console.error('Token refresh failed:', error); renewal.unableToRetrieveAuthToken(); } } }); // Use when logging in with backend-verified token await Client.login({ userId: 'user-123', displayName: 'John Doe', authToken: 'your-auth-token', }, sessionHandler); ``` ```Dart Function(AccessTokenRenewal) getProductionSessionHandler() { return (AccessTokenRenewal renewal) async { try { final myAuthToken = await getAuthTokenFromMyServer(); renewal.renewWithAuthToken(myAuthToken); } catch (error) { renewal.unableToRetrieveAuthToken(); } }; } void authenticateUser() async { // Use during login try { await AmityCoreClient.login( 'userId', sessionHandler: getProductionSessionHandler(), ).displayName('displayName').authToken('authToken').submit(); } catch (error) { // Handle authentication error } } ``` ## Alternative: Login with Access Token By default, social.plus SDK handles access token management for you — you provide a `userId` and an optional `authToken`, and the SDK takes care of obtaining and refreshing access tokens behind the scenes. **Login with Access Token** is an alternative authentication method for customers who want to manage the entire token lifecycle themselves. Instead of letting the SDK obtain tokens, your backend issues a JWT access token directly, and your app passes it to the SDK. **Most developers should use the default login flow** described in the [Quick Start](#quick-start-basic-authentication) section above. Only use this approach if you have a specific need listed below. ### When to Use This Your app already has a centralized authentication system and you want social.plus sessions to be issued as part of that flow — no extra network hop from the client. You want to eliminate the client-to-social.plus token exchange during login so that social.plus API availability does not block your app's core login experience. ### Default Login vs. Access Token Login | | **Default Login** | **Login with Access Token** | |---|---|---| | **SDK method** | `login(userId, authToken)` | `loginWithAccessToken(accessToken)` | | **Who obtains the access token?** | The SDK handles it automatically | Your backend issues a JWT and your app passes it to the SDK | | **Token renewal** | Session handler (`sessionWillRenewAccessToken`) | Access token handler (`onTokenRenew`) | | **Best for** | Most integrations | SSO, custom auth backends, decoupled architectures | | **Client-to-social.plus call during login?** | Yes — SDK exchanges credentials for a token | No — token is pre-issued by your backend | ### How It Works ```mermaid sequenceDiagram participant App as Your App participant Backend as Your Backend participant SDK as social.plus SDK participant API as social.plus API App->>Backend: Request access token for user Backend->>Backend: Generate & sign JWT Backend->>App: Return JWT access token App->>SDK: loginWithAccessToken(accessToken) SDK->>API: Verify token API->>SDK: Session established SDK->>App: Login complete Note over SDK,API: Later, when token is about to expire... SDK->>App: onTokenRenew(userId) App->>Backend: Request new access token Backend->>App: Return fresh JWT App->>SDK: Return new token SDK->>API: Verify & refresh session ``` ### Implementation Integration requires three steps: implement a token handler, register it, then login. Create a handler that the SDK will call whenever the token needs renewal. Your handler should request a fresh JWT from your backend. Call `setAccessTokenHandler()` **before** logging in. This tells the SDK how to obtain a new token when the current one expires. Call `loginWithAccessToken()` with the JWT your backend issued. The SDK verifies it with social.plus and establishes the session. ```swift // 1. Implement the AccessTokenHandler protocol. // The SDK calls `onTokenRenew` when the access token is expired // or about to expire. You must return a fresh JWT obtained from // your own authentication backend. class MyTokenHandler: AccessTokenHandler { func onTokenRenew(userId: String) async throws -> String { // Use the provided userId to request a new JWT // from your authentication backend. let newToken = try! await fetchNewTokenFromBackend(userId: userId) return newToken } } // 2. Register the handler BEFORE calling loginWithAccessToken. // The handler must be set first so the SDK can invoke it // whenever a token renewal is needed. let client = try! AmityClient(apiKey: "") let tokenHandler = MyTokenHandler() client.setAccessTokenHandler(tokenHandler) // 3. Now login with the initial access token. // On subsequent token renewals, the SDK will automatically // call onTokenRenew and re-authenticate using the returned JWT. try! await client.loginWithAccessToken(accessToken: initialAccessToken) ``` ```kotlin // 1. Implement the AccessTokenHandler interface. // The SDK calls `onTokenRenew` when the access token is expired // or about to expire. You must return a fresh JWT obtained from // your own authentication backend. val handler = object : AccessTokenHandler { override suspend fun onTokenRenew(userId: String): String { // get new token from your api; replace with real API call return "new-access-token" } } // 2. Register the handler BEFORE calling loginWithAccessToken. // The handler must be set first so the SDK can invoke it // whenever a token renewal is needed. AmityCoreClient.setAccessTokenHandler(handler) // 3. Now login with the initial access token. // On subsequent token renewals, the SDK will automatically // call onTokenRenew and re-authenticate using the returned JWT. AmityCoreClient.loginWithAccessToken(authToken) .build() .submit() .subscribe() ``` ```typescript import { Client } from "@amityco/ts-sdk"; // 1. Define custom token handler. // The SDK calls `onTokenRenew` when the access token is expired // or about to expire. You must return a fresh JWT obtained from // your own authentication backend. const tokenHandler = { async onTokenRenew(userId: string): Promise { // Request new JWT from your authentication backend const response = await fetch("https://your-backend.com/api/refresh-token", { method: "POST", credentials: "include", userId, }); const data = await response.json(); return data.accessToken; }, }; // 2. Register the handler BEFORE calling loginWithAccessToken. // The handler must be set first so the SDK can invoke it // whenever a token renewal is needed. Client.setAccessTokenHandler(tokenHandler); // 3. Now login with the initial access token. // On subsequent token renewals, the SDK will automatically // call onTokenRenew and re-authenticate using the returned JWT. await Client.loginWithAccessToken(initialAccessToken); ``` **Handler must be registered first.** Always call `setAccessTokenHandler()` before `loginWithAccessToken()`. If no handler is registered, the SDK cannot renew expired tokens and will throw an error. ## Security Best Practices ### Production Token Management Auth tokens must be generated by your secure backend to establish server-to-server trust with social.plus. Refer to the general API Reference introduction until the dedicated authentication endpoint page is published. ```javascript // Example Node.js backend - generates tokens for verified users app.post('/api/auth/social-plus-token', async (req, res) => { const { userId } = req.body; // STEP 1: Verify user is authenticated in YOUR system const user = await verifyUserInYourSystem(userId); if (!user) { return res.status(401).json({ error: 'User not authenticated in your system' }); } // STEP 2: Generate social.plus auth token for this verified user const authToken = generateSocialPlusToken(userId); // STEP 3: Return token to your app for social.plus login res.json({ authToken, message: 'Token generated for verified user' }); }); ``` **Why this approach?** - Your backend vouches for the user's authenticity to social.plus - social.plus trusts users who have valid tokens from your verified backend - Prevents unauthorized access to social.plus features - Maintains security boundary between your auth system and social.plus ### Authentication Best Practices - **Monitor session state changes** in your app's main navigation logic - **Handle token expiration gracefully** with automatic refresh - **Provide clear feedback** to users during state transitions - **Clean up subscriptions** when components unmount - **Use secure logout** when security is critical - **Store tokens securely** using platform-specific secure storage - **Don't ignore session state changes** - they indicate important authentication events - **Don't store sensitive data** when user is not authenticated - **Don't make API calls** before reaching `established` state - **`Don't forget to handle the terminated state`** - users may be banned - **Don't hardcode production API keys** in public source or logs - **Don't use plain text storage** for auth tokens ## Troubleshooting **Symptoms**: Login never completes, app shows loading indefinitely **Solutions**: 1. Check your API key and network connection 2. Verify authentication token is valid 3. Ensure you're using the correct region 4. Check for console errors or network timeouts **Symptoms**: Users get logged out too often **Solutions**: 1. Check your backend token expiration settings 2. Verify session handler implementation 3. Ensure token refresh logic works correctly 4. Consider longer token expiration for better UX **Symptoms**: Tokens don't refresh automatically **Solutions**: 1. Verify you're passing the session handler during login 2. Check that your backend token has appropriate expiration 3. Ensure session handler implementation handles errors 4. Test with shorter token expiration for debugging **Symptoms**: App crashes when authentication state changes **Solutions**: 1. Ensure UI updates happen on the main thread 2. Handle all possible session states in switch statements 3. Add proper null checks and error handling 4. Clean up observers when components unmount **Symptoms**: Calling `loginWithAccessToken()` throws an error before reaching the server **Solutions**: 1. Ensure you called `setAccessTokenHandler()` **before** `loginWithAccessToken()` — the handler must be registered first 2. Verify your JWT is well-formed (valid JSON Web Token structure) 3. Check that the JWT contains the required `userId` claim 4. Confirm your backend is signing the token with the key configured in the social.plus console **Symptoms**: Token expires but the SDK does not invoke your `onTokenRenew` handler **Solutions**: 1. Verify you logged in via `loginWithAccessToken()` — the handler is only invoked for access-token sessions, not default `login()` sessions 2. Check that the handler was registered with `setAccessTokenHandler()` before login 3. Ensure the user is not globally banned — the SDK skips handler invocation for banned users ## Next Steps Learn about user profiles and management Explore social capabilities structure Add real-time messaging Configure security and settings in the Console --- ### [Visitor Mode](https://learn.social.plus/social-plus-sdk/getting-started/visitor-mode) > Enable visitor mode to allow anonymous users to browse public content without signing in. Server-side read-only controls protect platform integrity. Visitor mode lets anonymous users browse public content read-only, without signing in. Access is controlled server-side to protect platform integrity. **Visitor mode is not enabled by default.** To enable visitor mode for your network, please contact [support@social.plus](mailto:support@social.plus) with your network details. ## Overview & Concepts ### What is Visitor Mode? Visitor mode enables **anonymous public access** to your community, allowing users to browse and discover content without requiring authentication. This feature is ideal for growth funnels, SEO optimization, and public content discovery while maintaining platform security and stability. **Purpose**: Anonymous users who browse public content\ **Access**: Read-only permissions enforced server-side\ **Tracking**: Identified by an SDK-generated device ID\ **Use Case**: Public content discovery, growth funnel **Purpose**: Search engine crawlers and automated indexers\ **Access**: Read-only permissions for content indexing\ **Tracking**: Identified by User-Agent analysis\ **Use Case**: SEO optimization, content discoverability ### How Visitor Mode Works Visitor mode uses an **SDK-generated device ID** to identify anonymous users while maintaining privacy: The SDK generates or retrieves a stable device ID for the anonymous user The SDK logs in the user as a visitor using the device ID, with optional secure mode via authSignature social.plus server assigns the "Visitor" or "Bot" role based on the request (User-Agent for bots) Server-side permissions enforce read-only access, allowing content discovery without modification capabilities ```mermaid sequenceDiagram participant Device as User Device participant App as Your App participant SDK as social.plus SDK participant API as social.plus API Device->>App: Access public content App->>SDK: getVisitorDeviceId() SDK->>SDK: Generate/Retrieve Device ID SDK->>App: Return Device ID App->>SDK: loginAsVisitor(deviceId) SDK->>API: POST /api/v5/sessions/visitor API->>API: Assign "Visitor" Role API->>SDK: Return Session (Read-Only) SDK->>App: Login Complete App->>Device: Show Public Content ``` **Why device IDs?** This approach lets social.plus distinguish anonymous visitor sessions without requiring a signed-in user account. Visitors are restricted to read-only access, protecting community integrity. ### User Type Comparison Understanding the different user types helps you design the right access patterns: ```typescript // Full authenticated user with read/write access import { Client } from '@amityco/ts-sdk'; const client = Client.createClient('your-api-key', 'sg'); // ... your app setup code await Client.login({ userId: 'user-123', displayName: 'John Doe', }, sessionHandler); ``` **Capabilities:** - ✅ Full read/write access to all features - ✅ Real-time event connections (MQTT) - ✅ Push notifications - ✅ Create posts, comments, reactions - ✅ Join communities and follow users ```typescript // Anonymous visitor with read-only access import { Client } from '@amityco/ts-sdk'; const client = Client.createClient('your-api-key', 'sg'); await Client.loginAsVisitor({ sessionHandler, }); ``` **Capabilities:** - ✅ View public posts and content - ✅ Browse public communities - ✅ View user profiles - ❌ No real-time connections (no MQTT) - ❌ No push notifications - ❌ Cannot create content or interact - ❌ Cannot join communities or follow users ```typescript // Search engine crawler with read-only access import { Client } from '@amityco/ts-sdk'; const client = Client.createClient('your-api-key', 'sg'); await Client.loginAsBot({ sessionHandler }); ``` **Capabilities:** - ✅ Index public content for SEO - ✅ View public posts and communities - ✅ Separated analytics tracking - ❌ No real-time connections - ❌ No push notifications - ❌ No write operations ## Parameters | Operation | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | --- | | Initialize SDK | API key | Yes | iOS, Android, TypeScript | Application API key from the social.plus Console. | | Initialize SDK | Region / endpoint | Yes | iOS, Android, TypeScript | Region where your social.plus application was created. | | Get visitor device ID | None | No | iOS, Android, TypeScript | SDK returns or generates the stable anonymous device ID. | | Login as visitor | `sessionHandler` | Recommended | iOS, Android, TypeScript | Token-renewal handler for visitor sessions. | | Secure visitor login | `authSignature` | Secure mode | iOS, Android, TypeScript | Backend-generated HMAC signature for the visitor device ID and expiration time. | | Secure visitor login | `authSignatureExpiresAt` | Secure mode | iOS, Android, TypeScript | Expiration timestamp that was included in the signature. | | Login as bot | `sessionHandler` | Recommended | TypeScript | Token-renewal handler for explicit bot sessions. | | Check user type | None | No | iOS, Android, TypeScript | SDK returns the current user type so the UI can adjust capabilities. | ## Quick Start (Visitor Mode) ### Step 1: Initialize the SDK Start by setting up the social.plus client with your API key: ```swift iOS let client = try! AmityClient(apiKey: "your-api-key", region: .SG) ``` ```kotlin Android AmityCoreClient.setup( apiKey = "your-api-key", endpoint = AmityEndpoint.SG ) ``` ```typescript TypeScript import { Client } from '@amityco/ts-sdk'; const client = Client.createClient('your-api-key', 'sg'); ``` ### Step 2: Get Visitor Device ID Generate or retrieve a unique device identifier for the visitor: ```swift iOS let deviceId = client.getVisitorDeviceId() print("Device ID: \(deviceId)") ``` ```kotlin Android val deviceId = AmityCoreClient.getVisitorDeviceId() Log.d("Visitor", "Device ID: $deviceId") ``` ```typescript TypeScript const client = Client.createClient('your-api-key', 'sg'); const deviceId = await client.getVisitorDeviceId(); console.log('Device ID:', deviceId); ``` The device ID is automatically generated and cached on first access. This unique identifier is used to track the visitor session. ### Step 3: Login as Visitor Authenticate as an anonymous visitor to access public content: ```swift iOS Task { @MainActor in do { // Simple visitor login (development) try await client.loginAsVisitor( authSignature: nil, authSignatureExpiresAt: nil, sessionHandler: sessionHandler ) print("Visitor login successful") } catch { print("Visitor login failed: \(error)") } } ``` ```kotlin Android // Simple visitor login (development) AmityCoreClient.loginAsVisitor(sessionHandler) .build() .submit() .doOnComplete { // Visitor login successful } .doOnError { error -> // Visitor login failed } .subscribe() ``` ```typescript TypeScript try { // Simple visitor login (development) await Client.loginAsVisitor({ sessionHandler }); console.log('Visitor login successful'); } catch (error) { console.error('Visitor login failed:', error); } ``` ### Step 4: Login as Bot (TypeScript Only) For search engine crawlers and automated indexers: ```typescript TypeScript try { await Client.loginAsBot({ sessionHandler }); console.log('Bot login successful'); } catch (error) { console.error('Bot login failed:', error); } ``` Bot login is automatically determined by User-Agent analysis on the server. Use this method when you need explicit bot role assignment. ### Step 5: Check User Type Verify the current user type to adapt your UI accordingly: ```swift iOS let userType = client.currentUserType switch userType { case .signedIn: print("User is authenticated") case .visitor: print("User is a visitor") case .bot: print("User is a bot") } ``` ```kotlin Android val userType = AmityCoreClient.getCurrentUserType() when (userType) { AmityUserType.SIGNED_IN -> Log.d("Auth", "User is authenticated") AmityUserType.VISITOR -> Log.d("Auth", "User is a visitor") AmityUserType.BOT -> Log.d("Auth", "User is a bot") } ``` ```typescript TypeScript import { Client, UserTypeEnum } from '@amityco/ts-sdk'; const userType = Client.getCurrentUserType(); switch (userType) { case UserTypeEnum.SIGNED_IN: console.log('User is authenticated'); break; case UserTypeEnum.VISITOR: console.log('User is a visitor'); break; case UserTypeEnum.BOT: console.log('User is a bot'); break; } ``` ### Step 6: Logout End the visitor session: ```swift iOS do { try await client.secureLogout() } catch { /// Handle error from revoking accessToken here } ``` ```kotlin Android AmityCoreClient.secureLogout() .doOnComplete { // Void } .doOnError { // Exception } .subscribe() ``` ```typescript TypeScript const handleSecureLogout = async () => { await Client.secureLogout(); }; handleSecureLogout(); ``` ## Secure Visitor Mode (Production) For production environments, secure visitor mode adds an extra layer of authentication by requiring cryptographic signatures for visitor sessions. Once secure mode is enabled, all visitor login requests must include a valid auth signature generated by your backend server. **Secure visitor mode is not enabled by default.** Even if visitor mode is enabled, secure mode must be enabled separately. Contact [support@social.plus](mailto:support@social.plus) to enable secure visitor mode for your network. ### Getting Your Visitor Secret After visitor secure mode is enabled for your network, retrieve your visitor application secret from the Console: Open your social.plus Console and go to **Settings** → **Integrations** Scroll to the **Visitor Secure Mode Setup** section (visible only after visitor secure mode is enabled) Create new secret and store it securely in your backend environment variables **Security Best Practice:** Never expose your **secret** in client-side code, mobile apps, or version control. This secret must remain on your backend server only. ### Backend Auth Signature Generation Your backend server must generate time-limited auth signatures using HMAC-SHA256 encryption: ```javascript // Complete Express.js backend example const express = require('express'); const crypto = require('crypto'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 3000; // Middleware to parse JSON request bodies app.use(express.json()); // Visitor auth signature endpoint app.post('/api/visitor/auth-signature', async (req, res) => { try { const { deviceId } = req.body; // Set expiration (e.g., 1 hour from now) const authSignatureExpiresAt = new Date(Date.now() + 3600000).toISOString(); // Create signature using HMAC-SHA256 with your visitor secret const message = `deviceId=${deviceId}&authSignatureExpiresAt=${authSignatureExpiresAt}`; const authSignature = crypto .createHmac('sha256', process.env.SOCIAL_PLUS_VISITOR_APP_SECRET) .update(message) .digest('hex'); res.json({ authSignature, authSignatureExpiresAt }); } catch (error) { console.error('Error generating auth signature:', error); res.status(500).json({ error: 'Failed to generate auth signature' }); } }); // Start server app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); ``` **Setup Instructions:** 1. Install dependencies: ```bash npm install express dotenv ``` 2. Create a `.env` file in your project root: ```env SOCIAL_PLUS_VISITOR_APP_SECRET=your_visitor_secret_from_console PORT=3000 ``` 3. Run the server: ```bash node server.js ``` **How It Works:** The signature is created by hashing the device ID and expiration timestamp with your secret key. social.plus servers verify the signature using the same secret, ensuring the request originated from your trusted backend. ### Secure Visitor Login Use auth signatures for production visitor sessions. Obtain `authSignature` via the API implemented in the previous step, and provide the corresponding values to the `loginAsVisitor()` function. ```swift iOS Task { @MainActor in do { // Get device ID let deviceId = client.getVisitorDeviceId() // Request auth signature from your backend let (signature, expiresAt) = try await fetchAuthSignature(deviceId: deviceId) // Login with secure mode try await client.loginAsVisitor( authSignature: signature, authSignatureExpiresAt: expiresAt, sessionHandler: sessionHandler ) print("Secure visitor login successful") } catch { print("Secure visitor login failed: \(error)") } } ``` ```kotlin Android val deviceId = AmityCoreClient.getVisitorDeviceId() // Request auth signature from your backend fetchAuthSignature(deviceId) { signature, expiresAt -> AmityCoreClient.loginAsVisitor(sessionHandler) .authSignature(signature) .authSignatureExpiresAt(expiresAt) .build() .submit() .doOnComplete { // Secure visitor login successful } .doOnError { error -> // Secure visitor login failed } .subscribe() } ``` ```typescript TypeScript try { const deviceId = await client.getVisitorDeviceId(); // Request auth signature from your backend const { authSignature, authSignatureExpiresAt } = await fetchAuthSignature(deviceId); // Login with secure mode await Client.loginAsVisitor({ authSignature, authSignatureExpiresAt, sessionHandler, }); console.log('Secure visitor login successful'); } catch (error) { console.error('Secure visitor login failed:', error); } ``` ### Session Handler for Token Renewal Implement session handlers to automatically refresh auth signatures: ```swift class VisitorSessionHandler: SessionHandler { func sessionWillRenewAccessToken(renewal: AccessTokenRenewal) { let deviceId = client.getVisitorDeviceId() Task { do { // Fetch new auth signature from your backend let (signature, expiresAt) = try await fetchAuthSignature(deviceId: deviceId) renewal.renewWithAuthSignature( authSignature: signature, authSignatureExpiresAt: expiresAt ) } catch { print("Failed to refresh visitor token: \(error)") renewal.unableToRetrieveAuthSignature() } } } } // Use during visitor login let sessionHandler = VisitorSessionHandler() try await client.loginAsVisitor( authSignature: signature, authSignatureExpiresAt: expiresAt, sessionHandler: sessionHandler ) ``` ```kotlin class VisitorSessionHandler : SessionHandler { override fun sessionWillRenewAccessToken(renewal: AccessTokenRenewal) { val deviceId = AmityCoreClient.getVisitorDeviceId() // Fetch new auth signature from your backend authRepository.fetchVisitorAuthSignature(deviceId) { authData -> if (authData != null) { renewal.renewWithAuthSignature( authData.signature, authData.expiresAt ) } else { renewal.unableToRetrieveAuthToken() } } } } // Use during visitor login val sessionHandler = VisitorSessionHandler() AmityCoreClient.loginAsVisitor(sessionHandler) .authSignature(signature) .authSignatureExpiresAt(expiresAt) .build() .submit() ``` ```typescript const createVisitorSessionHandler = (): Amity.SessionHandler => ({ sessionWillRenewAccessToken: async (renewal: Amity.AccessTokenRenewal) => { try { const deviceId = await client.getVisitorDeviceId(); // Fetch new auth signature from your backend const { authSignature, authSignatureExpiresAt } = await AuthService.fetchVisitorAuthSignature(deviceId); renewal.renewWithAuthSignature({ authSignature, authSignatureExpiresAt }); } catch (error) { console.error('Failed to refresh visitor token:', error); renewal.unableToRetrieveAuthSignature(); } } }); // Use during visitor login await Client.loginAsVisitor({ authSignature, authSignatureExpiresAt, sessionHandler: createVisitorSessionHandler(), }); ``` ## Understanding Visitor Permissions Visitor and bot users have **server-side enforced read-only permissions** to protect community integrity: - View public posts and content - Browse public communities - View user profiles - View comments and replies - View post reactions - Access public media (images, videos) - Create posts or stories - Comment or reply - React to posts/comments - Join communities - Follow/unfollow users - Report content or users - Send messages - Receive push notifications - Real-time event connections (MQTT) ### Permission Enforcement All visitor restrictions are enforced **server-side** - attempting restricted actions will result in permission errors: ```typescript Error Codes // TypeScript and Android server error codes for visitor/bot permission denial ServerError.VISITOR_PERMISSION_DENIED: 403999 ServerError.BOT_PERMISSION_DENIED: 403998 ``` ```typescript Example Error Handling try { await createPost({ text: 'Hello world' }); } catch (error) { if (String(error).includes('403999')) { // Show visitor upgrade prompt showVisitorWarning('Create an account or sign in to post'); } } ``` ### Resource Conservation Visitors and bots are excluded from resource-intensive features: **MQTT Connection**: Disabled for visitors/bots - No real-time event subscriptions - No live updates or notifications - Reduces server load and connection costs - Does not count towards CCU (Concurrent Connection Users) limits ```typescript // SDK automatically skips MQTT connection for visitors const userType = Client.getCurrentUserType(); if (userType === UserTypeEnum.VISITOR || userType === UserTypeEnum.BOT) { // mqtt.connect() is NOT called } ``` **Push Notifications**: Blocked for visitors/bots - Cannot register device tokens - Filtered out from notification recipient lists - Applies to all notification types - Prevents unpredictable costs from anonymous audience ```typescript // Push notification registration is blocked import { Client, UserTypeEnum } from '@amityco/ts-sdk'; const userType = Client.getCurrentUserType(); if (userType === UserTypeEnum.VISITOR || userType === UserTypeEnum.BOT) { // registerPushNotification() throws error or no-ops } ``` **User Listing/Search**: Hidden from results - Excluded from user search APIs - Not visible in followers/following lists - Hidden from user discovery features - Maintains authentic member directories ```typescript // Visitors are automatically filtered from user queries // Your queries return only signed-in users const users = await UserRepository.searchUserByDisplayName({ displayName: 'John' }); // Returns: only SIGNED_IN users, no VISITOR or BOT users ``` ## Daily Usage Limit Visitor and bot users share a **daily read request quota**. Once the quota is exhausted, all subsequent read API calls return error code `400323` until the quota resets. **Quota**: 100 read requests per day, shared across all API endpoints — feed, events, communities, user profiles, etc. The counter resets daily. Monthly overages are a billing concern only; the SDK never receives a monthly-limit error. ### Error Code | Error Constant | TypeScript / Android Code | iOS Code | Trigger | | --- | --- | --- | --- | | `VISITOR_USAGE_LIMIT_EXCEEDED` | `400323` | `.visitorUsageLimitExceeded` / `400323` | Visitor/bot has exhausted their daily read quota | | `VISITOR_PERMISSION_DENIED` | `403999` | `.visitorPermissionDenied` / `488999` | Visitor attempted a restricted operation | | `BOT_PERMISSION_DENIED` | `403998` | `.botPermissionDenied` / `488998` | Bot attempted a restricted operation | ### SDK Event Subscription The Android and TypeScript SDKs emit a visitor usage-limit event the first time error `400323` is detected per session. Subsequent failures within a 2-second window are deduplicated to avoid triggering the handler on simultaneous parallel requests. ```kotlin // Subscribe to usage limit events after visitor login AmityCoreClient.getVisitorUsageLimitEvents() .observeOn(AndroidSchedulers.mainThread()) .doOnNext { event -> // Navigate to sign-in or show custom error UI Log.d("Visitor", "Usage limit reached for user: ${event.userId}") } .subscribe() ``` ```typescript import { Client } from '@amityco/ts-sdk'; // Subscribe to usage limit events after visitor login const unsubscribe = Client.onVisitorUsageLimitReached(() => { // Navigate to sign-in or show custom error UI console.log('Visitor usage limit reached'); }); // Unsubscribe when cleaning up // unsubscribe(); ``` The event is only emitted for `VISITOR` and `BOT` user types. Signed-in users never receive this event. ## Data Management & Lifecycle ### Guest User Data Cleanup To prevent accumulation of transient visitor data, social.plus automatically cleans up inactive guest users: **Schedule**: Periodic cleanup (configurable, typically 30-60 days) **Criteria**: Guest users inactive for the defined period **Process**: - Scheduled job runs automatically - Identifies inactive guest user records - Permanently deletes inactive guest data - No manual intervention required **What's Deleted**: - Guest user profile records - Visitor device ID associations - Session history - Any cached visitor data **Active Visitors**: Continuously using visitors retain their data **Privacy Compliance**: Automatic cleanup supports GDPR/privacy regulations **Analytics Impact**: Historical analytics remain unaffected **Conversion Tracking**: Converted visitors (who signed up) preserve their history **Event Availability**: Guest user events are available through existing webhook/event observation mechanisms configured in your social.plus console. ## Implementation Best Practices ### Visitor Mode Strategy **Recommended Scenarios:** 1. **Public Content Discovery** - Community showcases and landing pages - SEO-optimized public content - Growth funnel entry points - Social media linked content 2. **Conversion Optimization** - Allow browsing before signup - Demonstrate community value - Reduce friction in user journey - Track engagement before conversion 3. **SEO & Indexing** - Enable search engine crawling - Improve content discoverability - Separate bot traffic from analytics - Optimize for organic search **Implementation Tips:** - Set clear upgrade prompts for interactive features - Track visitor-to-member conversion rates - Monitor guest traffic patterns - Use analytics to optimize conversion flow **Require Sign-In For:** 1. **Private/Sensitive Content** - Member-only communities - Personal conversations - Restricted content - Premium features 2. **High-Value Interactions** - Content creation - Community moderation - Direct messaging - Transaction-based features 3. **Compliance Requirements** - Age-restricted content - Regulated industries - Terms of service acceptance - User accountability needs ### Security Considerations **Always Use Secure Mode in Production:** ```typescript // ✅ Production: Secure visitor mode with auth signatures await Client.loginAsVisitor({ authSignature, // Generated by your backend authSignatureExpiresAt, // With proper expiration sessionHandler, // With token renewal logic }); // ❌ Development only: Simple visitor mode await Client.loginAsVisitor({ sessionHandler }); // No auth signature ``` **Why Secure Mode?** - Prevents unauthorized visitor creation - Enables server verification of device identity - Supports automatic token renewal - Maintains audit trail of visitor sessions **Privacy-conscious approach:** - Visitor device IDs are pseudonymous SDK identifiers - Visitor mode does not require a signed-in user profile - Automatic cleanup of inactive visitors - Your app remains responsible for its own privacy notice and consent requirements **Best Practices:** - Disclose visitor tracking in privacy policy - Provide opt-out mechanisms where required - Use device IDs only for platform functionality - Don't link device IDs to external identifiers ## Troubleshooting **Symptoms**: Cannot login as visitor, permission denied errors **Solutions**: 1. **Verify visitor mode is enabled** - Contact [support@social.plus](mailto:support@social.plus) if visitor mode has not been enabled for your network 2. Check API key has visitor access permissions 3. Ensure you're using correct region endpoint 4. For secure mode, verify visitor secure mode is enabled for your network 5. For secure mode, verify auth signature is correctly generated 6. Check auth signature hasn't expired ## Next Steps Learn about authenticated user login and session management Understand user profiles and member management Configure community permissions and access levels Track visitor metrics and conversion analytics in the Console --- ### [Android](https://learn.social.plus/social-plus-sdk/getting-started/platform-setup/mobile/android-quick-start) > Get up and running with social.plus Android SDK for Kotlin/Java applications in minutes. Get your Android app connected to social.plus in just a few steps. This guide covers everything from installation to your first authenticated session. ## Requirements - Android 6.0 (API 23)+ - Target SDK 36+ - Compile SDK 36+ - JVM target 1.8 - Kotlin 2.2.0+ ## Installation Add the SDK to your project using your preferred repository: Add Maven Central to your project-level `build.gradle`: ```gradle Gradle 6.8+ dependencyResolutionManagement { repositories { mavenCentral() } } ``` ```gradle Gradle 6.7 and below allprojects { repositories { mavenCentral() } } ``` Add the dependency to your module-level `build.gradle`: {/* doc-as-test: skip(build-script: Gradle DSL fragment, not runtime Kotlin) */} ```kotlin Gradle (Kotlin DSL) implementation("co.amity.android:amity-sdk:x.y.z") ``` ```gradle Gradle (Groovy) implementation 'co.amity.android:amity-sdk:x.y.z' ``` Replace `x.y.z` with the [latest version number](https://github.com/AmityCo/Amity-Social-Cloud-Android-SDK/releases). If your minSDKVersion is below 24, there are additional configurations required. In your project build.gradle: ``` buildscript { repositories { google() gradlePluginPortal() ... } dependencies { ... classpath 'gradle.plugin.com.github.sgtsilvio.gradle:android-retrofix:0.4.1' } } ``` Add Jitpack to your project-level `build.gradle`: ```gradle allprojects { repositories { maven { url 'https://jitpack.io' } } } ``` Add the dependency: ```gradle implementation 'com.github.AmityCo:Amity-Social-Cloud-Android-SDK:x.y.z' ``` ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Initialize SDK | API key | Yes | Application API key from the social.plus Console. | | Initialize SDK | Endpoint | No | Region endpoint such as `AmityEndpoint.US`, `EU`, or `SG`; SG is the default when omitted. | | Initialize SDK with encryption | `dbEncryption` | No | Database encryption mode: `NONE`, `AUTH`, or `ALL`. | | Initialize SDK with encryption | Encryption key | Required when encryption is enabled | Stable key bytes supplied to the selected database encryption mode. | | Build configuration | SDK version | Yes | Android SDK version in the Gradle dependency declaration. | ## Initialize the SDK Set up the client with your API key so the SDK is ready for authentication - highly recommended to do this on the application class. ```kotlin Android AmityCoreClient.setup(apiKey = "YOUR_API_KEY") ``` This creates the SDK client but does not yet authenticate a user. See [Authentication](/social-plus-sdk/getting-started/authentication) for the next step. ## Configuration ### Managing conflicting file generation In your app module's build.gradle, add the following packaging options. ```gradle android { ... packagingOptions { exclude 'META-INF/INDEX.LIST' exclude 'META-INF/io.netty.versions.properties' } } ``` ### ProGuard Rules If using ProGuard, add these rules to your `proguard-rules.pro`: ```pro -keep class com.ekoapp.ekosdk.** { *; } -keep interface com.ekoapp.ekosdk.** { *; } -keep enum com.ekoapp.ekosdk.** { *; } -keep class com.amity.socialcloud.** { *; } -keep interface com.amity.socialcloud.** { *; } -keep enum com.amity.socialcloud.** { *; } -keep class co.amity.rxupload.** { *; } ``` If you are using the SDK version below 6.9.0. If you'd like to pass an Amity Serializable Object such as AmityPost, AmityMessage, etc. You will need to add ProGuard rules below: ```pro -keepclassmembers class * implements java.io.Serializable { private static final java.io.ObjectStreamField[] serialPersistentFields; private void writeObject(java.io.ObjectOutputStream); private void readObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); } ``` ### Log Visibility Configuration To control log visibility, add the following to your `build.gradle`: ```gradle android { defaultConfig { resValue 'bool', "IS_HIDDEN_AMITY_LOG", "true" … } … } ``` ## Database Encryption (Optional) The SDK does not employ database encryption by default. The database file is solely restricted to the application by the operating system, which is generally sufficient for most use cases. Database encryption serves as an additional layer of security in the event of compromised root access. **Important**: Enabling database encryption may lead to a performance reduction of up to 15% during database read/write operations. ### Encryption Modes **No Encryption** Default mode with no encryption applied. Best performance. **Token Security** Access token storage is encrypted. **Recommended** for balanced security. **Full Encryption** All database files are encrypted. Maximum security. **AUTH mode is recommended** to introduce extra security with minimal performance compromise. Choose the encryption mode that aligns with your application's specific requirements. ### Implementation ```kotlin fun setupWithDatabaseEncryption(encryptionKey: String) { AmityCoreClient.setup( apiKey = "your-api-key", dbEncryption = AmityDBEncryption.AUTH(encryptionKey.toByteArray()) ) } ``` ```kotlin fun setupWithFullEncryption(encryptionKey: String) { AmityCoreClient.setup( apiKey = "your-api-key", dbEncryption = AmityDBEncryption.ALL(encryptionKey.toByteArray()) ) } ``` ```kotlin fun setupWithoutEncryption() { AmityCoreClient.setup( apiKey = "your-api-key", dbEncryption = AmityDBEncryption.NONE ) } ``` ### Encryption Key Management Enabling database encryption requires an encryption key. **You must consistently pass the same key** to the SDK. If a new key is supplied, the existing database will be erased and regenerated with the new key. The level of security depends on your key generation and storage method. Follow industry standards for both key storage and generation. **Add the security library dependency:** ```gradle implementation 'androidx.security:security-crypto:1.1.0-alpha06' ``` **Secure key implementation:** {/* doc-as-test: skip(android: uses EncryptedSharedPreferences/MasterKeys not in compile classpath) */} ```kotlin private fun getEncryptionKey(context: Context): String { // Use androidx.security:security-crypto to store generated key val securedSharedPreferences = EncryptedSharedPreferences.create( "amity_encrypted_shared_prefs", MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC), context, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) val cachedKey = securedSharedPreferences.getString("amity_key", null) return if (cachedKey != null) { cachedKey } else { // Generate AES 256-bit key val keyGen = KeyGenerator.getInstance("AES") keyGen.init(256) val key = keyGen.generateKey() val encodedKey = key.encoded val generatedKey = String(Base64.encode(encodedKey, Base64.DEFAULT)) // Store the key securely securedSharedPreferences.edit() .putString("amity_key", generatedKey) .apply() generatedKey } } ``` **Usage in your Application class:** {/* doc-as-test: skip(android-ui: Application class body, not standalone function) */} ```kotlin class MyApplication : Application() { override fun onCreate() { super.onCreate() // Get or generate encryption key val encryptionKey = getEncryptionKey(this) // Setup SDK with encryption AmityCoreClient.setup( apiKey = "your-api-key", endpoint = AmityEndpoint.SG, dbEncryption = AmityDBEncryption.AUTH(encryptionKey.toByteArray()) ) } } ``` ### Performance Considerations **Performance Impact**: Database encryption adds computational overhead. Consider these factors: - **AUTH mode**: ~5-8% performance impact - **ALL mode**: ~10-15% performance impact - **Battery usage**: Slightly increased due to encryption/decryption operations - **Storage**: Minimal impact on storage size ## Next Steps Learn about session management and secure authentication flows Start building chat and messaging features Add posts, feeds, and social interactions Implement live video and streaming features ## Troubleshooting **Dependency conflicts**: Use the latest versions and ensure all Amity dependencies use the same version **ProGuard issues**: Make sure you've added the required ProGuard rules **Minimum SDK version**: Ensure your app's minimum SDK is at least API 23 **SDK not initialized**: Make sure you call `AmityCoreClient.setup()` in your Application class **Authentication failures**: Verify your API key and region settings **Network errors**: Check internet permissions and network connectivity **Missing RxJava**: The SDK uses RxJava 2&3 - ensure you have the dependency added **Threading issues**: Use `.subscribeOn(Schedulers.io())` for background operations **Memory leaks**: Always dispose of your subscriptions in onDestroy() --- ### [Flutter](https://learn.social.plus/social-plus-sdk/getting-started/platform-setup/mobile/flutter-quick-start) > Get up and running with social.plus Flutter SDK for cross-platform applications in minutes. Get your Flutter app connected to social.plus in just a few steps. This guide covers everything from installation to your first authenticated session. ## Requirements - Flutter 3.0.0 - 4.0.0 - Dart SDK >=3.0.0 <4.0.0 - iOS 12.0+ / Android 4.4 (API 19)+ ## Installation Add the social.plus SDK to your `pubspec.yaml`: ```yaml dependencies: flutter: sdk: flutter amity_sdk: ^x.y.z # Check latest version ``` Run the installation command: ```bash flutter pub get ``` `pubspec.yaml` via pub.dev is the only supported installation method for the social.plus Flutter SDK. Find the latest version at [pub.dev/packages/amity_sdk](https://pub.dev/packages/amity_sdk). If you need to pin to a specific version or git ref, use Dart's standard [git-dependency syntax](https://dart.dev/tools/pub/dependencies#git-packages) in `pubspec.yaml` — no separate alternative install path is required. ## Project Configuration Before you initialize the Flutter SDK, make sure the generated iOS and Android projects meet the native platform requirements. These native settings usually only need to be configured once per app, but they must be in place before you run the SDK on a device or simulator. After changing either native project configuration, rerun `flutter pub get` and rebuild the app so the generated platform projects pick up the updated settings. Use the native Flutter project files below as your source of truth for these platform-level settings. ### iOS Configuration Set the minimum iOS deployment target in `ios/Podfile`. The Flutter SDK example project uses a Podfile with the standard Flutter target setup, and you should set the platform version explicitly before running `pod install`. ```ruby Podfile platform :ios, '12.0' target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end ``` The Flutter SDK example Podfile includes the standard Runner target and leaves the `platform` line commented as `12.0`; set it explicitly in your app so CocoaPods resolves the SDK against the correct deployment target. ### Android Configuration Keep your Android minimum SDK at Android 4.4 / API 19 or higher. The Flutter SDK example app inherits Flutter's default through `flutter.minSdkVersion`, so set an explicit value if your project overrides that default. ```groovy build.gradle android { defaultConfig { applicationId "com.example.example" minSdkVersion 19 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } } ``` The Flutter SDK example app uses `minSdkVersion flutter.minSdkVersion` in `example/android/app/build.gradle`; if you override it in your own app, keep the value at 19 or above to match the SDK requirements. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Install SDK | Package version | Yes | `amity_sdk` version declared in `pubspec.yaml`. | | Initialize SDK | API key | Yes | Application API key from the social.plus Console. | | Initialize SDK | Endpoint | No | Region endpoint passed through `AmityCoreClientOption` where your app needs a non-default region. | | Native project setup | iOS deployment target | Yes | Minimum iOS version in `ios/Podfile`; keep it at 12.0 or above. | | Native project setup | Android min SDK | Yes | Android minimum SDK version; keep it at API 19 or above. | ## Initialize the SDK Create the client with your API key so the SDK is ready for authentication. ```dart Flutter Future initializeSDK() async { await AmityCoreClient.setup( option: AmityCoreClientOption.create( apiKey: 'YOUR_API_KEY', ), ); } ``` This creates the SDK client but does not yet authenticate a user. See [Authentication](/social-plus-sdk/getting-started/authentication) for the next step. ## Next Steps Learn about session management and secure authentication flows Start building chat and messaging features Add posts, feeds, and social interactions Implement live video and streaming features ## Troubleshooting **Package not found**: Make sure you've added the correct package name to pubspec.yaml **Version conflicts**: Use `flutter pub deps` to check for dependency conflicts **Build errors**: Run `flutter clean` and `flutter pub get` to refresh dependencies **SDK not initialized**: Make sure you call `AmityCoreClient.setup()` before using any SDK features **Authentication failures**: Verify your API key and region settings **Session state issues**: Ensure you're properly listening to session state changes **iOS build fails**: Ensure minimum iOS version is set to 12.0+ **Android build fails**: Check that minimum SDK is set to 19+ --- ### [iOS](https://learn.social.plus/social-plus-sdk/getting-started/platform-setup/mobile/ios-quick-start) > Get up and running with social.plus iOS SDK for Swift applications in minutes. Get your iOS app connected to social.plus in just a few steps. This guide covers everything from installation to your first authenticated session. ## Requirements - Xcode 26.0+ - iOS 14.0+ - Swift 5.0+ ## Installation Add the social.plus SDK to your project using Swift Package Manager: 1. In Xcode, select **File** → **Add Package Dependencies** 2. Enter the repository URL: ``` https://github.com/AmityCo/Amity-Social-Cloud-SDK-iOS-SwiftPM ``` 3. Select **Up to Next Major Version** and click **Add Package** 4. Choose **AmitySDK** and click **Add Package** Use "Up to Next Major Version" to ensure compatibility with future updates. 1. [Download the latest iOS SDK](https://sdk.amity.co/sdk-release/ios/amitysdk.zip) 2. Drag `AmitySDK.xcframework` to your project 3. Select **Copy items if needed** and click **Finish** 4. Set the **Embed** option to **Embed & Sign** For M1 Macs using the simulator, enable Rosetta mode for the application. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Initialize SDK | API key | Yes | Application API key from the social.plus Console. | | Initialize SDK | Region | Yes | Region passed to `AmityClient`, such as `.US`, `.EU`, or `.SG`. | | Objective-C bridge login | `userId` | Yes | Stable user identifier from your identity system. | | Objective-C bridge login | `displayName` | No | Display name passed through the Swift bridge. | | Objective-C bridge login | `authToken` | Production | Backend-generated auth token passed through the Swift bridge. | | Objective-C bridge login | Completion callback | Yes | Callback that reports success or failure back to Objective-C. | ## Initialize the SDK Create the client with your API key and region so the SDK is ready for authentication. ```swift iOS import AmitySDK let client = try! AmityClient(apiKey: "YOUR_API_KEY", region: .SG) ``` This creates the SDK client but does not yet authenticate a user. See [Authentication](/social-plus-sdk/getting-started/authentication) for the next step. ## Objective-C Integration Starting with v6.0.0, AmitySDK for iOS is written in **Pure Swift**. You can still use it in Objective-C projects by creating a **Mixed-Language Project**. We recommend integrating AmitySDK directly into your Objective-C project and using **Swift language** to call the SDK interfaces for better compatibility and performance. ### Mixed Language Project Setup Create Swift files with necessary interfaces/methods that interact with AmitySDK. These interfaces should be exposed with `@objc` or `@objcMembers` attributes. When you add a new Swift file to your Objective-C project, Xcode automatically generates a bridging header file that exposes your Swift code to Objective-C. 📖 **Learn More**: [Importing Swift into Objective-C - Apple Developer](https://developer.apple.com/documentation/swift/importing-swift-into-objective-c) ### Implementation Example Create a Swift file that wraps AmitySDK functionality: ```swift // SDKLoginManager.swift // Example of a Swift file which contains a class to interact with AmitySDK import AmitySDK class NoopSessionHandler: SessionHandler { func sessionWillRenewAccessToken(renewal: any AccessTokenRenewal) {} } @objc class SDKLoginManager: NSObject { let client: AmityClient? @objc init(apiKey: String) { self.client = try? AmityClient(apiKey: apiKey) } @objc func login(userId: String, displayName: String, authToken: String, completion: @escaping (Bool, Error?) -> Void) { Task { do { try await self.client?.login( userId: userId, displayName: displayName.isEmpty ? nil : displayName, authToken: authToken.isEmpty ? nil : authToken, sessionHandler: NoopSessionHandler() ) completion(true, nil) } catch { completion(false, error) } } } @objc func logout() { Task { try? await self.client?.secureLogout() } } @objc func isLoggedIn() -> Bool { guard let state = self.client?.sessionState else { return false } if case .established = state { return true } return false } @objc func getCurrentUserId() -> String? { return self.client?.currentUserId ?? nil } } ``` Use the Swift bridge in your Objective-C code: ```objc // ViewController.m #import "ViewController.h" #import "YourProjectName-Swift.h" // <- This import exposes your Swift file @interface ViewController () @property (nonatomic, strong) SDKLoginManager *loginManager; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Initialize the SDK manager self.loginManager = [[SDKLoginManager alloc] initWithApiKey:@"your-api-key"]; } - (void)performLogin { [self.loginManager loginWithUserId:@"user-123" displayName:@"John Doe" authToken:@"your-auth-token" completion:^(BOOL isSuccess, NSError * _Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ if (isSuccess) { NSLog(@"Login successful"); // Navigate to main app [self showMainApp]; } else { NSLog(@"Login failed: %@", error.localizedDescription); // Show error message [self showErrorAlert:error.localizedDescription]; } }); }]; } - (void)performLogout { [self.loginManager logout]; NSLog(@"User logged out"); } - (void)checkLoginStatus { if ([self.loginManager isLoggedIn]) { NSString *userId = [self.loginManager getCurrentUserId]; NSLog(@"User is logged in: %@", userId); } else { NSLog(@"User is not logged in"); } } // Helper methods - (void)showMainApp { // Your navigation logic here } - (void)showErrorAlert:(NSString *)message { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Login Error" message:message preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]; [alert addAction:okAction]; [self presentViewController:alert animated:YES completion:nil]; } @end ``` ### Key Considerations **Bridging Header**: Xcode automatically creates a bridging header when you add Swift files to an Objective-C project. **Import Statement**: Use `#import "YourProjectName-Swift.h"` to access Swift classes in Objective-C. **Target Membership**: Ensure your Swift files are added to the correct target. **Minimize Bridge Calls**: Create comprehensive wrapper methods rather than making frequent cross-language calls. **Error Handling**: Properly handle Swift optionals and errors in your Objective-C code. **Threading**: Always dispatch UI updates to the main queue when handling completion callbacks. **Module Not Found**: Ensure your project's module name doesn't contain special characters or spaces. **Swift Version**: Make sure your Objective-C project supports the Swift version used by AmitySDK. **Linker Errors**: Verify that both Objective-C and Swift files are properly linked to your target. ## Next Steps Learn about session management and secure authentication flows Start building chat and messaging features Add posts, feeds, and social interactions Implement live video and streaming features ## Troubleshooting **Framework not found**: Ensure you've added all required frameworks and set them to "Embed & Sign" **Swift version conflicts**: Ensure your project uses Swift 5.0 or later **SDK not initialized**: Make sure you call `AmityClient.setup(...)` in `application` delegate **Authentication failures**: Verify your API key and region settings **Permission denied**: Check that required permissions are added to Info.plist **M1 Mac Simulator Issues**: Enable Rosetta for your application in Xcode --- ### [TypeScript](https://learn.social.plus/social-plus-sdk/getting-started/platform-setup/web/web-quick-start) > Get up and running with social.plus TypeScript SDK for web applications in minutes. Get your web application connected to social.plus in just a few steps. This guide covers everything from installation to your first authenticated session. ## Requirements ### Browser Support - Chrome 38+ - Firefox 42+ - Safari 9+ - Microsoft Edge 13+ - Opera 25+ Internet Explorer 11 is not supported. The SDK requires modern browser features. ### Framework Compatibility - Create React App - Next.js (client-side) - Vite React - Custom React setups - Vue 3 composition API - Vue 2 options API - Nuxt.js (client-side) - Vite Vue - Angular 12+ - Angular CLI projects - Nx workspaces - Custom Angular setups - React Native - Svelte/SvelteKit - Solid.js - Lit Framework ## Installation ```bash npm install @amityco/ts-sdk --save ``` ```bash yarn add @amityco/ts-sdk ``` ```bash pnpm add @amityco/ts-sdk ``` ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Initialize SDK | API key | Yes | Application API key from the social.plus Console. | | Initialize SDK | Region | Yes | Region string passed to `Client.createClient`, such as `us`, `eu`, or `sg`. | | Initialize SDK | Client options | No | Optional TypeScript client configuration, such as custom endpoints where supported. | ## Initialize the SDK Create the client with your API key and region so the SDK is ready for authentication. ```typescript TypeScript import { Client } from '@amityco/ts-sdk'; const client = Client.createClient('YOUR_API_KEY', 'sg'); ``` This creates the SDK client but does not yet authenticate a user. See [Authentication](/social-plus-sdk/getting-started/authentication) for the next step. ## Next Steps Learn about session management and secure authentication flows Start building chat and messaging features Add posts, feeds, and social interactions Implement live video and streaming features ## Troubleshooting **Module not found**: Make sure you've installed the package correctly with your package manager **TypeScript errors**: Ensure you're using TypeScript 3.7+ and have proper type definitions **Build errors**: Check that your bundler supports ES6 modules and async/await **Network failures**: Verify your API key and region settings **Authentication errors**: Check that your auth token is valid and not expired **Memory leaks**: Always call `Client.logout()` when your app unmounts **Older browsers**: Make sure you've included the required polyfills **Safari issues**: Ensure you're using HTTPS in production (required for WebRTC features) ## Core Concepts — Users ### [Overview](https://learn.social.plus/social-plus-sdk/core-concepts/user-management/overview) > Understand the SDK pages for user identity, user operations, roles, and moderation workflows. User management in social.plus starts with your own identity system. Your app supplies a stable `userId`, then the SDK uses that ID for login, profile reads, profile updates, search, permissions, and moderation actions. social.plus does not replace your account system. Keep private identity data in your backend, and send only the stable user ID plus social profile fields your product needs. ## Section Map Choose stable user IDs and understand which social profile fields the SDK stores. Create users, retrieve profiles, search users, update profiles, flag users, and manage token workflows. Check whether the current user can perform actions in global, channel, or community scopes. Add user reporting flows for moderator review. ## Implementation Flow Use an immutable ID from your own system, such as a database primary key or UUID. Call the SDK `login` method. If the `userId` does not exist yet, the SDK creates the user. Use user repository APIs for profile reads, search, query, and current-user profile updates. Use permission checks for protected actions and flagging APIs for reporting workflows. ## Quick Start 1. **[User Identity](/social-plus-sdk/core-concepts/user-management/user-identity)** - Decide what `userId` your app will send to social.plus. 2. **[Create User](/social-plus-sdk/core-concepts/user-management/user-operations/create-user)** - Log in or create the user through the SDK. 3. **[Get User Information](/social-plus-sdk/core-concepts/user-management/user-operations/get-user-information)** - Retrieve profiles for known users or user lists. 4. **[Search and Query Users](/social-plus-sdk/core-concepts/user-management/user-operations/search-and-query-users)** - Build user pickers, directories, or moderation lists. 5. **[Roles & Permissions](/social-plus-sdk/core-concepts/user-management/roles-permissions)** - Gate protected actions by user capability. ## Data Boundary | Data | Owner | SDK behavior | | --- | --- | --- | | Login credentials, email, private profile data | Your app/backend | Keep this outside social.plus. | | `userId` | Your app/backend | Sent to social.plus as the immutable user identifier. | | Display name, description, avatar, metadata | social.plus stores what you send | Use only social profile fields needed by your product. | | Roles, permissions, moderation state | social.plus and Console/API workflows | Read or check through SDK methods where available. | --- ### [User Identity](https://learn.social.plus/social-plus-sdk/core-concepts/user-management/user-identity) > Use stable user IDs with social.plus SDK while keeping private identity data in your own system. social.plus SDK identifies each user by a `userId` that your app provides. The `userId` should be stable, unique, and non-sensitive because it becomes the SDK identity used by login, profile, search, permission, and moderation APIs. Keep your source-of-truth account record, credentials, email address, and private profile data in your own system. Send social.plus only the stable `userId` and the social profile fields your experience needs. ## User ID Rules | Rule | Recommendation | | --- | --- | | Stable | Use an ID that will not change for the lifetime of the account. | | Unique | Use a value already guaranteed unique by your backend. | | Non-sensitive | Avoid emails, phone numbers, or other personal identifiers. | | Reusable across SDK calls | Use the same value for login, profile reads, search results, and moderation actions. | ## Recommended IDs Use database primary keys, UUIDs, or another immutable ID from your backend. ```text userId: "12345" userId: "user_abc123" userId: "4f8b4c2d-9e1a-4f3a-8b7c-6d5e4f3a9910" ``` Do not use identifiers that may change or expose private information. ```text userId: "john.doe@example.com" userId: "johndoe123" userId: "john_doe" ``` After a user is created in social.plus, treat the `userId` as immutable. ## Stored Social Profile Fields social.plus stores social profile and moderation fields that support SDK features. Avoid putting sensitive personal data in these fields. | Field | Description | | --- | --- | | `userId` | Stable identifier supplied by your app. | | `displayName` | User-facing profile name. | | `description` | User-facing profile description or bio. | | `metadata` | Custom social metadata. Do not store sensitive personal data here. | | `avatarFileId` / `avatarCustomUrl` | Avatar image reference. | | `roles` | Assigned roles for permissions and moderation workflows. | | `flagCount` / `isFlaggedByMe` | Moderation reporting state. | | `isGlobalBan` / `isDeleted` | Account moderation or deletion state exposed by SDK query results where available. | ## Parameters | Operation | Input | Required | Platforms | Description | | --- | --- | --- | --- | --- | | Initialize user repository | Initialized SDK client | Yes | TypeScript, iOS, Android, Flutter | The SDK must be initialized before user repository APIs are used. | | Initialize user repository | User repository object or import | Yes | TypeScript, iOS, Android, Flutter | Platform-specific entry point for user reads, search, updates, and moderation operations. | | Use user operation APIs | `userId`, display-name keyword, sort option, or paging control | Operation-dependent | TypeScript, iOS, Android, Flutter | Use the operation pages for method-specific inputs and result shapes. | ## Initialize the user repository Initialize the user repository before calling user read, search, update, or moderation APIs. ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; ``` ```swift iOS let userRepository = AmityUserRepository() ``` ```kotlin Android fun initUserRepository() { val userRepository = AmityCoreClient.newUserRepository() } ``` ```dart Flutter void initUserRepository() { final userRepository = AmityCoreClient.newUserRepository(); } ``` ## Related topics Log in or create a user with the stable `userId`. Choose the right SDK operation for user reads, updates, search, moderation, and tokens. --- ### [Roles & Permissions](https://learn.social.plus/social-plus-sdk/core-concepts/user-management/roles-permissions) > Check whether the current user can perform protected actions with social.plus SDK permission APIs. Use `hasPermission` to decide whether the current user should see or perform protected actions such as deleting messages, editing community posts, or banning users. Permission checks are scope-aware: some checks are global, while others are evaluated for a channel or community. Permissions are assigned through roles and membership state. The SDK checks the permissions already available for the current user and returns a Boolean-style result. ## Parameters | Operation | Required inputs | Optional inputs | Platforms | Result shape | | --- | --- | --- | --- | --- | | Check global permission | Permission constant | None | TypeScript, iOS, Android, Flutter | Boolean or observable Boolean result. | | Check channel permission | Permission constant, `channelId` | None | TypeScript, iOS, Android, Flutter | Boolean or observable Boolean result. | | Check community permission | Permission constant, `communityId` | None | TypeScript, iOS, Android, Flutter | Boolean or observable Boolean result. | ## Check a permission Use the scope that matches the action you are gating. For example, message deletion inside a channel should use a channel-scoped permission check, while community post editing should use a community-scoped check. ### Inputs | Platform | Method | Required inputs | Result shape | | --- | --- | --- | --- | | TypeScript | `client.hasPermission(permission).currentUser()`, `.channel(channelId)`, or `.community(communityId)` | Permission string, optional scope ID | Returns `boolean`. | | iOS | `client.hasPermission(permission)`, `forChannel:`, or `forCommunity:` | `AmityPermission`, optional scope ID | Returns `Bool` from an async call. | | Android | `AmityCoreClient.hasPermission(permission).atGlobal()`, `.atChannel(channelId)`, or `.atCommunity(communityId).check()` | `AmityPermission`, optional scope ID | Returns `Flowable`. | | Flutter | `AmityCoreClient.hasPermission(permission).atGlobal()`, `.atChannel(channelId)`, or `.atCommunity(communityId).check()` | `AmityPermission`, optional scope ID | Returns `bool`. | ```typescript TypeScript import { Client } from '@amityco/ts-sdk'; const client = Client.createClient('your-api-key', 'sg'); const canEditCommunityPost = client .hasPermission('EDIT_COMMUNITY_POST') .community('community_123'); if (canEditCommunityPost) { // Enable edit controls. } ``` ```swift iOS Task { @MainActor in let canDeleteMessage = await client.hasPermission( .deleteMessage, forChannel: "channel_123" ) if canDeleteMessage { // Enable delete controls. } } ``` ```kotlin Android fun checkChannelPermission() { AmityCoreClient.hasPermission(AmityPermission.DELETE_MESSAGE) .atChannel("channel_123") .check() .doOnNext { canDeleteMessage: Boolean -> if (canDeleteMessage) { // Enable delete controls. } } .subscribe() } ``` ```dart Flutter void checkChannelPermission(String channelId) { final canMute = AmityCoreClient .hasPermission(AmityPermission.MUTE_USER_INSIDE_CHANNEL) .atChannel(channelId) .check(); if (canMute) { // Enable mute controls. } } ``` ## Platform notes - TypeScript permission checks are synchronous Boolean checks against current cached user, channel membership, or community membership data. - iOS permission checks are async and return `Bool`. - Android permission checks return `Flowable`. - Flutter permission checks return `bool`. - Use the permission constant that matches the action and scope. Permission names differ by platform enum casing, but they map to server permission strings such as `DELETE_MESSAGE`, `EDIT_COMMUNITY_POST`, and `BAN_USER`. ## Related topics Understand the stable `userId` used by permission checks. Add user reporting actions for moderation workflows. --- ### [Create User](https://learn.social.plus/social-plus-sdk/core-concepts/user-management/user-operations/create-user) > Learn how to create new users in social.plus SDK through the login method social.plus SDK creates new users through the `login` method. A single call to `login` creates the account if the `userId` does not exist, or authenticates the user if it does. The `login` method serves dual purposes: it creates new users when they don't exist and authenticates existing users when they do. ## How user creation works When you call the `login` method: 1. **Existing user**: If a user exists with the specified `userId`, the SDK logs them in and optionally updates their `displayName`. 2. **New user**: If no user exists with the `userId`, the SDK creates a new account and logs them in automatically. ## Parameters | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | | `userId` | Yes | TypeScript, iOS, Android, Flutter | Unique user identifier. Maximum length: 50 characters. | | `displayName` | No | TypeScript, iOS, Android, Flutter | User-facing display name. Maximum length: 100 characters. | | `authToken` | No | TypeScript, iOS, Android, Flutter | Secure token used when your app enables secure mode. | | `sessionHandler` | Platform-dependent | TypeScript, iOS, Android, Flutter | Handles access-token renewal for authenticated sessions. | ## Log in or create a user Call the platform login method with a `userId`; include `displayName`, `authToken`, and session renewal handling when your integration needs them. ```typescript TypeScript import { Client } from '@amityco/ts-sdk'; const sessionHandler: Amity.SessionHandler = { sessionWillRenewAccessToken(renewal) { renewal.renew(); }, }; async function loginUser(userId: string, displayName?: string, authToken?: string) { Client.createClient('your-api-key', 'sg'); await Client.login( { userId, displayName, authToken, }, sessionHandler, ); } await loginUser('user_123', 'Bob Newton'); ``` ```swift iOS Task { @MainActor in do { try await client.login( userId: "", displayName: "<(optional)-display-name>", authToken: "<(optional)-auth-token>", sessionHandler: sessionHandler ) print("login success") } catch { print("login failed \(error)") } } ``` ```kotlin Android fun authenticate() { AmityCoreClient.login(userId = "userId 1", sessionHandler = null) .displayName(displayName = "John Doe") // optional .authToken(authToken = "token") // optional .build() .submit() .doOnComplete { //success } .subscribe() } ``` ```dart Flutter void login() async { await AmityCoreClient.login('userId', sessionHandler: (AccessTokenRenewal renewal) { renewal.renew(); }) .displayName('userDisplayName') .authToken('token') // optional .submit(); } ``` ## Related topics Learn how to retrieve user data and profiles. Discover how to modify user profiles and settings. --- ### [Get User Information](https://learn.social.plus/social-plus-sdk/core-concepts/user-management/user-operations/get-user-information) > Retrieve user profiles by ID, batch user lookups where supported, and query paginated user collections. Retrieve user profiles for profile screens, user cards, and directory views with the user repository. Use the single-user API when you know the exact user ID, batch lookup when your SDK exposes `getUserByIds`, and `getUsers()` when you need a sorted list instead of a fixed set of IDs. iOS and TypeScript expose live-object or live-collection wrappers for user observation. Android uses `Flowable` for the same operations, while Flutter returns `Future` for `getUser(userId)` and query builders for paginated collections. ## Parameters This page covers three user-read operations. Use this table to choose the operation first, then use the inputs table in each section for the exact SDK call shape. | Operation | Use when | Required inputs | Platforms | | --- | --- | --- | --- | | Get a single user | You already know one user ID and need that profile. | `userId` | TypeScript, iOS, Android, Flutter | | Get multiple users by ID | You already have a fixed set of user IDs and need those profiles together. | `userIds` | TypeScript, Android | | Query users | You need a sorted or paginated user list rather than a fixed set of IDs. | None globally; sort options vary by platform. | TypeScript, iOS, Android, Flutter | ## Get a single user Use the single-user API when your app already knows the user ID and needs the latest profile details for that specific user. The examples below show the native return shape for each SDK. ### Inputs | Platform | Method | Required inputs | Result shape | | --- | --- | --- | --- | | TypeScript | `UserRepository.getUser(userId, callback)` | `userId`, callback | Starts a live observer and returns an unsubscriber. | | iOS | `userRepository.getUser(userId)` | `userId` | Returns a live object observed with an `AmityNotificationToken`. | | Android | `userRepository.getUser(userId)` | `userId` | Returns `Flowable`. | | Flutter | `AmityCoreClient.newUserRepository().getUser(userId)` | `userId` | Returns `Future`. | ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; const unsubscribe = UserRepository.getUser('user_123', ({ data: user, loading, error }) => { if (loading) return; if (error || !user) { console.error('Failed to get user', error); return; } console.log(user.userId, user.displayName); }); // Call unsubscribe() when the screen no longer needs user updates. ``` ```swift iOS var token: AmityNotificationToken? func observeUser() { let liveObject = userRepository.getUser("user_123") token = liveObject.observe { liveObject, error in guard let user = liveObject.snapshot else { print("error: \(String(describing: error))") return } print("userId: \(user.userId), displayName: \(String(describing: user.displayName))") } } ``` ```kotlin Android fun getUser(userRepository: AmityUserRepository) { userRepository.getUser(userId = "user_123") .doOnNext { user: AmityUser -> val displayName = user.getDisplayName() val userId = user.getUserId() Log.d("UserRepo", "User: $displayName ($userId)") } .doOnError { error -> Log.e("UserRepo", "Failed to get user", error) } .subscribe() } ``` ```dart Flutter Future getUser() async { try { final user = await AmityCoreClient.newUserRepository().getUser('user_123'); print('User: ${user.displayName}'); } on AmityException catch (error) { print('Failed to get user: $error'); } } ``` ## Get multiple users by ID Use batch lookup when your app already has a fixed set of user IDs and needs those profiles together. This API is available on TypeScript and Android. For browse flows, use the query operation below. ### Inputs | Platform | Method | Required inputs | Result shape | | --- | --- | --- | --- | | TypeScript | `UserRepository.getUserByIds(userIds)` | `userIds: string[]` | Returns a promise with cached user data. | | Android | `userRepository.getUserByIds(userIds)` | `userIds: Set` | Returns `Flowable>`. | ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; async function getUsersByIds() { const userIds = ['user_123', 'user_456']; const { data: users } = await UserRepository.getUserByIds(userIds); users.forEach(user => { console.log(user.userId, user.displayName); }); } ``` ```kotlin Android fun getUsersByIds( userRepository: AmityUserRepository, userIds: Set ) { userRepository.getUserByIds(userIds = userIds) .doOnNext { users: List -> users.forEach { user -> Log.d("UserRepo", "User: ${user.getDisplayName()}") } } .doOnError { error -> Log.e("UserRepo", "Failed to get users", error) } .subscribe() } ``` ## Query users Use `getUsers()` when you need a sorted, paginated user list for browse flows such as user directories, member pickers, or moderation tools. On TypeScript, the live collection callback also exposes `hasNextPage` and `onNextPage` when more results are available. ### Inputs | Platform | Method | Required inputs | Optional inputs | Result shape | | --- | --- | --- | --- | --- | | TypeScript | `UserRepository.getUsers(params, callback)` | callback | `sortBy`, `limit` | Starts a live collection observer and returns an unsubscriber. | | iOS | `userRepository.getUsers(sortBy)` | sort option | None in this call shape | Returns a live collection observed with an `AmityNotificationToken`. | | Android | `userRepository.getUsers().build().query()` | None | `sortBy(...)` | Returns `Flowable>`. | | Flutter | `AmityCoreClient.newUserRepository().getUsers()` | None | `sortBy(...)`, paging token, limit | Returns paging data through the query builder. | ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; let loadMoreUsers: (() => void) | undefined; const unsubscribe = UserRepository.getUsers( { sortBy: 'lastCreated' }, ({ data: users, loading, error, hasNextPage, onNextPage }) => { if (loading) return; if (error) { console.error('Failed to query users', error); return; } console.log(`Loaded ${users.length} users`); console.log(`More pages available: ${hasNextPage}`); loadMoreUsers = onNextPage; }, ); // Call loadMoreUsers?.() from your load-more action. // Call unsubscribe() when the screen no longer needs user list updates. ``` ```swift iOS var token: AmityNotificationToken? func queryUsersExample() { let liveCollection = userRepository.getUsers(.displayName) token = liveCollection.observe { collection, error in let users = collection.snapshots print("Loaded \(users.count) users") } } ``` ```kotlin Android fun queryUsers(userRepository: AmityUserRepository) { userRepository.getUsers() .sortBy(sortOption = AmityUserSortOption.DISPLAYNAME) // optional .build() .query() .doOnNext { users: PagingData -> // PagingData } .doOnError { // Exception } .subscribe() } ``` ```dart Flutter final _amityUsers = []; late PagingController _amityUsersController; void getUsers(AmityUserSortOption amityUserSortOption) { _amityUsersController = PagingController( pageFuture: (token) => AmityCoreClient.newUserRepository() .getUsers() .sortBy(amityUserSortOption) .getPagingData(token: token, limit: 20), pageSize: 20, )..addListener( () { if (_amityUsersController.error == null) { _amityUsers.clear(); _amityUsers.addAll(_amityUsersController.loadedItems); } }, ); } ``` ## Platform notes - `getUserByIds(userIds)` is public on TypeScript and Android, but not on iOS or Flutter. - TypeScript `getUser(...)` and `getUsers(...)` start live observers and return unsubscriber functions when your screen no longer needs updates. - Android uses `Flowable` for single-user observation and `Flowable>` for batch lookup. - Flutter `getUser(userId)` is a one-time `Future`. If you need stream-based observation there, use `AmityCoreClient.newUserRepository().live.getUser(userId)`. ## Related topics Search users by display name and query user collections. Update the authenticated user's profile data. --- ### [Update User Information](https://learn.social.plus/social-plus-sdk/core-concepts/user-management/user-operations/update-user-information) > Update the current user's display name, description, avatar, and metadata with social.plus SDK. Use the user update APIs when the signed-in user changes their social profile. Client SDKs update the current user's profile fields; privileged edits to other users should be handled through admin or backend workflows. Before setting an avatar file, upload the image first. See [Image Handling](/social-plus-sdk/core-concepts/content-handling/files-images-and-videos/image-handling#image-upload) for the upload flow. Only update fields that changed. The SDK update builders and patch objects let you send partial profile updates. ## Parameters | Field | Required | Platforms | Description | | --- | --- | --- | --- | | `userId` | TypeScript and Flutter | TypeScript, Flutter | User ID for the profile being updated. Use the current user's ID for client-side updates. | | `displayName` | No | TypeScript, iOS, Android, Flutter | User-facing profile name. | | `description` | No | TypeScript, iOS, Android, Flutter | User-facing profile description or bio. | | `avatarFileId` / uploaded avatar | No | TypeScript, iOS, Android, Flutter | Uploaded image reference used as the user's avatar. | | `avatarCustomUrl` | No | TypeScript, iOS, Android, Flutter | Custom avatar URL where supported. | | `metadata` | No | TypeScript, iOS, Android, Flutter | Custom social metadata. Do not store sensitive personal data here. | ## Update the current user's profile Call the platform update method with only the fields you want to change. ### Inputs | Platform | Method | Required inputs | Optional inputs | Result shape | | --- | --- | --- | --- | --- | | TypeScript | `UserRepository.updateUser(userId, patch)` | `userId`, patch object | `displayName`, `description`, `avatarFileId`, `avatarCustomUrl`, `metadata` | Returns `Promise>`. | | iOS | `client.editUser(options)` | `AmityUserUpdateOptions` | Display name, description, avatar, avatar custom URL, metadata | Async call that throws on failure. | | Android | `AmityCoreClient.editUser().build().apply()` | None beyond the active user session | `displayName`, `description`, `avatar`, `avatarCustomUrl`, `metadata` | Returns `Single`. | | Flutter | `AmityCoreClient.newUserRepository().updateUser(userId).update()` | `userId` | `displayName`, `description`, `avatarFileId`, `avatarCustomUrl`, `metadata`, `roles` | Returns `Future`. | ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; async function updateUserProfile(userId: string) { const { data: user } = await UserRepository.updateUser(userId, { displayName: 'Batman', description: 'Hero that Gotham needs', metadata: { city: 'Gotham', }, }); console.log('Updated user:', user.displayName); } ``` ```swift iOS func updateUserProfile() async { let options = AmityUserUpdateOptions() options.setDisplayName("Batman") options.setUserDescription("Hero that Gotham needs") options.setUserMetadata(["city": "Gotham"]) do { try await client.editUser(options) print("User updated") } catch { print("Update failed: \(error)") } } ``` ```kotlin Android fun updateUserProfile() { AmityCoreClient.editUser() .displayName(displayName = "Batman") .description(description = "Hero that Gotham needs") .metadata(metadata = JsonObject().apply { addProperty("city", "Gotham") }) .build() .apply() .doOnSuccess { user: AmityUser -> Log.d("UserUpdate", "Updated user: ${user.getDisplayName()}") } .doOnError { error -> Log.e("UserUpdate", "Update failed", error) } .subscribe() } ``` ```dart Flutter Future updateUserProfile(String userId) async { try { final user = await AmityCoreClient.newUserRepository() .updateUser(userId) .displayName('Batman') .description('Hero that Gotham needs') .metadata({'city': 'Gotham'}) .update(); print('Updated user: ${user.displayName}'); } on AmityException catch (error) { print('Update failed: ${error.message}'); } } ``` ## Platform notes - iOS and Android update the active user through `client.editUser(...)` / `AmityCoreClient.editUser()`. - TypeScript and Flutter take a `userId` in the update call. In client apps, pass the current user's ID. - iOS `setAvatar(...)` expects uploaded `AmityImageData`; Android `avatar(...)` expects uploaded `AmityImage`; TypeScript and Flutter use avatar file IDs or custom URLs. - Do not store sensitive personal data in `metadata`. ## Related topics Retrieve updated user profiles. Choose stable user IDs and safe social profile fields. --- ### [Delete User](https://learn.social.plus/social-plus-sdk/core-concepts/user-management/user-operations/delete-user) > Understand why user deletion is not exposed through client SDK user operations. Client SDKs do not expose a public method to delete a user. Treat user deletion as a privileged admin or backend workflow, not as a client-side SDK operation. Do not put user deletion credentials or privileged deletion flows in a client app. ## SDK Availability | Surface | Availability | | --- | --- | | TypeScript SDK | No public delete-user method | | iOS SDK | No public delete-user method | | Android SDK | No public delete-user method | | Flutter SDK | No public delete-user method | | Admin/API workflow | Use the Console or a backend-controlled API integration. | ## Recommended Workflow Verify the requester is allowed to delete or deactivate the user account. Keep API credentials and audit logging outside the client app. Refresh user lists, member lists, profile views, and moderation tools after the backend operation completes. SDK query results may expose deleted-user state such as `isDeleted`; make your UI resilient to missing names or avatars. ## Alternatives | Approach | Use when | | --- | --- | | Suspend or ban user | You need to block access without permanently deleting account data. | | Remove or moderate content | The issue is content-specific rather than account-specific. | | Hide user locally | You need a product-level visibility rule before a backend account decision is made. | ## Related topics Handle deleted-user state in profile and list views. Gate privileged admin and moderation actions. --- ### [Search and Query Users](https://learn.social.plus/social-plus-sdk/core-concepts/user-management/user-operations/search-and-query-users) > Search for users by display name and query paginated user collections in social.plus SDK. Use the user repository when your app needs to find people by display name or browse a paginated user list. Search is for keyword-driven flows such as member pickers, while query is for directory-style lists where sorting and pagination matter more than a search keyword. Deleted users are automatically excluded from search and query results. ## Parameters This page covers two user-list operations. Choose the operation first, then use the inputs table in each section for the exact SDK call shape. | Operation | Use when | Required inputs | Platforms | | --- | --- | --- | --- | | Search users by display name | You have a display-name keyword and want matching users. | Display-name keyword, callback or collection observer where required. | TypeScript, iOS, Android, Flutter | | Query users | You need a paginated user list without a search keyword. | Callback or sort option where required. | TypeScript, iOS, Android, Flutter | ## Search users by display name Use search when the user types a display-name keyword. Search keywords must be at least 3 characters long. When a keyword is provided, the server ranks matching results by search relevance; supported sort options can then control the returned order. ### Inputs | Platform | Method | Required inputs | Optional inputs | Result shape | | --- | --- | --- | --- | --- | | TypeScript | `UserRepository.searchUserByDisplayName(params, callback)` | `displayName`, callback | `limit`, `matchType`, `searchBy` | Starts a live collection observer and returns an unsubscriber. | | iOS | `userRepository.searchUsers(displayName, sortBy:, matchType:)` | `displayName`, `sortBy` | `matchType` | Returns a live collection observed with an `AmityNotificationToken`. | | Android | `userRepository.searchUsers(keyword).build().query()` | `keyword` | `sortBy(...)`, `matchType(...)` | Returns `Flowable>`. | | Flutter | `AmityCoreClient.newUserRepository().searchUserByDisplayName(keyword)` | `keyword` | `sortBy(...)`, `matchType(...)`, paging token, limit | Returns paging data through the query builder. | TypeScript search parameters intentionally do not include `sortBy`; use `getUsers(...)` when you need TypeScript user-list sorting. ### Special character handling With display-name sorting, users are sorted alphabetically by their display names using ICU collation for the English locale. This means that special characters such as Ä are treated as variants of A. For example, a sorted list might appear as: **adam, Älex, Alice, Arthur, charlie, Kristen**. When providing a search keyword, the API performs an exact-match lookup for special characters: - Searching for "Äli" only returns users whose display name contains "Äli", such as "Älise". - Searching for "Alice" does not return "Älice". Use the platform search method to retrieve users whose display name matches the search keyword. ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; let loadMoreUsers: (() => void) | undefined; const unsubscribe = UserRepository.searchUserByDisplayName( { displayName: 'Joe', limit: 20 }, ({ data: users, loading, error, hasNextPage, onNextPage }) => { if (loading) return; if (error) { console.error('Failed to search users', error); return; } console.log(`Found ${users.length} users`); console.log(`More pages available: ${hasNextPage}`); loadMoreUsers = onNextPage; }, ); // Call loadMoreUsers?.() from your load-more action. // Call unsubscribe() when the screen no longer needs search updates. ``` ```swift iOS var token: AmityNotificationToken? func searchUserExample() { let liveCollection = userRepository.searchUsers( "", sortBy: .displayName, matchType: .default ) token = liveCollection.observe { collection, error in let users = collection.snapshots print("Found \(users.count) users") } } ``` ```kotlin Android fun searchUsers(userRepository: AmityUserRepository) { userRepository.searchUsers("Brian") .sortBy(sortOption = AmityUserSortOption.DISPLAYNAME) // optional .build() .query() .doOnNext { users: PagingData -> // PagingData } .doOnError { error -> Log.e("UserRepo", "Failed to search users", error) } .subscribe() } ``` ```dart Flutter final _amityUsers = []; late PagingController _amityUsersController; void searchUserByDisplayName(String keyword) { _amityUsersController = PagingController( pageFuture: (token) => AmityCoreClient.newUserRepository() .searchUserByDisplayName(keyword) .sortBy(AmityUserSortOption.DISPLAY) .matchType(AmityUserSearchMatchType.DEFAULT) .getPagingData(token: token, limit: 20), pageSize: 20, )..addListener( () { if (_amityUsersController.error == null) { _amityUsers.clear(); _amityUsers.addAll(_amityUsersController.loadedItems); } }, ); } ``` ## Query users Use `getUsers()` when you need a paginated user list without a display-name keyword. TypeScript query sorting supports `firstCreated` and `lastCreated`; iOS, Android, and Flutter also expose display-name sorting. ### Inputs | Platform | Method | Required inputs | Optional inputs | Result shape | | --- | --- | --- | --- | --- | | TypeScript | `UserRepository.getUsers(params, callback)` | callback | `sortBy`, `limit`, `filter`, `matchType` | Starts a live collection observer and returns an unsubscriber. | | iOS | `userRepository.getUsers(sortBy)` | `sortBy` | None in this call shape | Returns a live collection observed with an `AmityNotificationToken`. | | Android | `userRepository.getUsers().build().query()` | None | `sortBy(...)` | Returns `Flowable>`. | | Flutter | `AmityCoreClient.newUserRepository().getUsers()` | None | `sortBy(...)`, paging token, limit | Returns paging data through the query builder. | Deleted users are excluded from query results. Use the platform query method when you need a paginated user list rather than keyword search. ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; let loadMoreUsers: (() => void) | undefined; const unsubscribe = UserRepository.getUsers( { sortBy: 'lastCreated', limit: 20 }, ({ data: users, loading, error, hasNextPage, onNextPage }) => { if (loading) return; if (error) { console.error('Failed to query users', error); return; } console.log(`Loaded ${users.length} users`); console.log(`More pages available: ${hasNextPage}`); loadMoreUsers = onNextPage; }, ); // Call loadMoreUsers?.() from your load-more action. // Call unsubscribe() when the screen no longer needs user list updates. ``` ```swift iOS var token: AmityNotificationToken? func queryUsersExample() { let liveCollection = userRepository.getUsers(.displayName) token = liveCollection.observe { collection, error in let users = collection.snapshots print("Loaded \(users.count) users") } } ``` ```kotlin Android fun queryUsers(userRepository: AmityUserRepository) { userRepository.getUsers() .sortBy(sortOption = AmityUserSortOption.DISPLAYNAME) // optional .build() .query() .doOnNext { users: PagingData -> // PagingData } .doOnError { error -> Log.e("UserRepo", "Failed to query users", error) } .subscribe() } ``` ```dart Flutter final _amityUsers = []; late PagingController _amityUsersController; void getUsers(AmityUserSortOption sortOption) { _amityUsersController = PagingController( pageFuture: (token) => AmityCoreClient.newUserRepository() .getUsers() .sortBy(sortOption) .getPagingData(token: token, limit: 20), pageSize: 20, )..addListener( () { if (_amityUsersController.error == null) { _amityUsers.clear(); _amityUsers.addAll(_amityUsersController.loadedItems); } }, ); } ``` ## Platform notes - TypeScript `searchUserByDisplayName(...)` starts a live collection observer, but its search params do not expose `sortBy`. - iOS search and query methods require an `AmityUserSortOption`. - Android and Flutter default user search/query sorting to display name when no explicit sort is provided. - Use pagination controls from the callback, live collection, `PagingData`, or Flutter `PagingController` instead of loading all users at once. ## Related topics Retrieve one user, batch lookup users where supported, or query user collections. Modify user profile fields. --- ### [Flag and Unflag Users](https://learn.social.plus/social-plus-sdk/core-concepts/user-management/user-operations/flag-unflag-user) > Flag users, remove your flag, and check user flag state with social.plus SDK. Use user flagging when a member reports another user for moderator review. Flagging does not block, ban, or delete the user; it records reporting state that moderation workflows can review. Flutter exposes flag and unflag actions from an `AmityUser` object. TypeScript, iOS, and Android expose repository methods that take a `userId`. ## Parameters | Operation | Required inputs | Platforms | Result shape | | --- | --- | --- | --- | | Flag user | `userId` | TypeScript, iOS, Android | TypeScript returns `Promise`; iOS async call throws on failure; Android returns `Completable`. | | Flag user | `AmityUser` object | Flutter | Returns `Future`. | | Unflag user | `userId` | TypeScript, iOS, Android | TypeScript returns `Promise`; iOS async call throws on failure; Android returns `Completable`. | | Unflag user | `AmityUser` object | Flutter | Returns `Future`. | | Check flag status | `userId` or `AmityUser` object | TypeScript, iOS, Android, Flutter | Boolean flag state where the platform exposes it. | ## Flag a user Call the flag API when the current user reports another user. ### Inputs | Platform | Method | Required inputs | Result shape | | --- | --- | --- | --- | | TypeScript | `UserRepository.flagUser(userId)` | `userId` | Returns `Promise`. | | iOS | `userRepository.flagUser(withId:)` | `userId` | Async call that throws on failure. | | Android | `userRepository.flagUser(userId)` | `userId` | Returns `Completable`. | | Flutter | `user.report().flag()` | `AmityUser` object | Returns `Future`. | ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; async function flagUser(userId: string) { const flagged = await UserRepository.flagUser(userId); console.log('Flagged:', flagged); } ``` ```swift iOS func flagUser() async { do { try await userRepository.flagUser(withId: "") print("User flagged") } catch { print("Flag failed: \(error)") } } ``` ```kotlin Android fun flagUser(userRepository: AmityUserRepository, userId: String) { userRepository.flagUser(userId = userId) .doOnComplete { Log.d("UserFlag", "User flagged") } .doOnError { error -> Log.e("UserFlag", "Flag failed", error) } .subscribe() } ``` ```dart Flutter Future flagUser(AmityUser user) async { try { final updatedUser = await user.report().flag(); print('Flagged user: ${updatedUser.userId}'); } on AmityException catch (error) { print('Flag failed: ${error.message}'); } } ``` ## Unflag a user Call the unflag API when the current user removes their report from another user. ### Inputs | Platform | Method | Required inputs | Result shape | | --- | --- | --- | --- | | TypeScript | `UserRepository.unflagUser(userId)` | `userId` | Returns `Promise`. | | iOS | `userRepository.unflagUser(withId:)` | `userId` | Async call that throws on failure. | | Android | `userRepository.unflagUser(userId)` | `userId` | Returns `Completable`. | | Flutter | `user.report().unflag()` | `AmityUser` object | Returns `Future`. | ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; async function unflagUser(userId: string) { const unflagged = await UserRepository.unflagUser(userId); console.log('Unflagged:', unflagged); } ``` ```swift iOS func unflagUser() async { do { try await userRepository.unflagUser(withId: "") print("User unflagged") } catch { print("Unflag failed: \(error)") } } ``` ```kotlin Android fun unflagUser(userRepository: AmityUserRepository, userId: String) { userRepository.unflagUser(userId = userId) .doOnComplete { Log.d("UserFlag", "User unflagged") } .doOnError { error -> Log.e("UserFlag", "Unflag failed", error) } .subscribe() } ``` ```dart Flutter Future unflagUser(AmityUser user) async { try { final updatedUser = await user.report().unflag(); print('Unflagged user: ${updatedUser.userId}'); } on AmityException catch (error) { print('Unflag failed: ${error.message}'); } } ``` ## Check whether the current user flagged a user Use the flag-state API or property to decide whether to show a flag or unflag action. ### Inputs | Platform | Method | Required inputs | Result shape | | --- | --- | --- | --- | | TypeScript | `UserRepository.isUserFlaggedByMe(userId)` | `userId` | Returns `Promise`. | | iOS | `userRepository.isUserFlaggedByMe(withId:)` | `userId` | Returns `Bool` from an async call. | | Android | `user.isFlaggedByMe()` | `AmityUser` object | Returns `Boolean` from the loaded user model. | | Flutter | `user.isFlaggedByMe` | `AmityUser` object | Returns `bool` from the loaded user model. | ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; async function checkFlagState(userId: string) { const isFlagged = await UserRepository.isUserFlaggedByMe(userId); console.log('Flagged by me:', isFlagged); } ``` ```swift iOS func checkFlagState() async { do { let isFlagged = try await userRepository.isUserFlaggedByMe(withId: "") print("Flagged by me: \(isFlagged)") } catch { print("Flag-state check failed: \(error)") } } ``` ```kotlin Android fun checkFlagState(user: AmityUser) { val isFlaggedByMe = user.isFlaggedByMe() val totalFlagCount = user.getFlagCount() Log.d("UserFlag", "Flagged by me: $isFlaggedByMe, total: $totalFlagCount") } ``` ```dart Flutter void checkFlagState(AmityUser user) { final isFlaggedByMe = user.isFlaggedByMe; print('Flagged by me: $isFlaggedByMe'); } ``` ## Platform notes - Flagging is a report signal for moderation review; it does not automatically block, ban, or delete the user. - TypeScript, iOS, and Android flag by `userId`. - Flutter flags through the loaded `AmityUser` object. - Android and Flutter expose flag status from the loaded user model. ## Related topics Retrieve user details and flag status. Gate moderation actions with permission checks. --- ### [User Token Management](https://learn.social.plus/social-plus-sdk/core-concepts/user-management/user-operations/user-token-management) > Learn how to manage user authentication tokens and credentials in social.plus SDK Use `AmityUserTokenManager` or the TypeScript `createUserToken(...)` helper to create user credentials for flows that require a user access token. This includes access to some beta features and server-side API workflows. The generated access token is not used by normal client SDK login. Create and consume these tokens only from a backend, tool, or trusted workflow where the token is not exposed to end users. ## Parameters | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | | `apiKey` | Yes | TypeScript, iOS, Android, Flutter | API key used to create the token manager or token request. Keep it in a trusted runtime when generating tokens. | | `region` / `endpoint` | Yes | TypeScript, iOS, Android, Flutter | Region or endpoint used to route the token request. | | `userId` | Yes | TypeScript, iOS, Android, Flutter | Unique identifier of the user whose credentials are created. | | `displayName` | No | TypeScript, iOS, Android, Flutter | Display name associated with the user credentials when provided. | | `authToken` / `secureToken` | No | TypeScript, iOS, Android, Flutter | Secure authentication token used when your app enables secure mode. | ## Create a user token To create a user token, pass the user's identifier and optional secure-mode credentials from a trusted workflow. Create the token with the SDK surface for your platform. ```typescript TypeScript import { API_REGIONS, createUserToken } from '@amityco/ts-sdk'; const { accessToken } = await createUserToken('your-api-key', API_REGIONS.SG, { userId: 'user_123', displayName: 'Jane Doe', authToken: 'secure-token', }); console.log(accessToken); ``` ```swift iOS // 1. create AmityUserTokenManager instance. let userTokenManager = AmityUserTokenManager(apiKey: "", region: .SG) func createNewUserTokenExample() async { // 2. call `createUserToken` on AmityUserTokenManager. do { let auth = try await userTokenManager.createUserToken( userId: "", displayName: "<(optional)-display-name>", authToken: "<(optional)-auth-token>" ) print("auth.accessToken: \(auth.accessToken)") } catch { print("unable to create a new user token: \(error.localizedDescription)") } } ``` ```kotlin Android fun createUserToken( userId: String, displayName: String, apiKey: String, endpoint: AmityEndpoint, authToken: String? ) { AmityUserTokenManager( apiKey = apiKey, endpoint = endpoint ).createUserToken( userId = userId, displayName = displayName, secureToken = authToken ) .doOnSuccess { userToken : AmityUserToken -> // AmityUserToken val accessToken = userToken.accessToken } .doOnError { // Exception } .subscribe() } ``` ```dart Flutter void createUserToken(String userId, String displayname, String secureToken) { AmityUserTokenManager( apiKey: "your api key", endpoint: AmityRegionalHttpEndpoint.SG) //displayname and secureToken are optional .createUserToken(userId, displayname: displayname, secureToken: secureToken) .then((AmityUserToken token) { log("accessToken = ${token.accessToken}"); }); } ``` ## Token security - Store tokens securely on the server side. - Use encryption for token storage. - Implement token rotation policies. - Never expose tokens in client-side code. - Always use HTTPS for token transmission. - Implement proper authentication headers. - Use secure communication channels. - Log token usage for audit purposes. ## Best Practices - Cache tokens to avoid unnecessary creation. - Implement token validation before usage. - Use connection pooling for better performance. - Handle token expiration gracefully. - Implement comprehensive error handling. - Log all token operations for debugging. - Provide meaningful error messages. - Implement retry logic for transient failures. - Batch token operations when possible. - Use background processing for token creation. - Implement caching strategies. - Monitor token usage patterns. ## Related Topics Learn about standard user login and authentication. Explore social.plus API documentation. ## Core Concepts — Content ### [File Handling](https://learn.social.plus/social-plus-sdk/core-concepts/content-handling/files-images-and-videos/file) > Upload, read, and delete generic file attachments with the Social+ SDKs. Use file handling when your app needs to upload a generic attachment such as a PDF, document, archive, or audio-adjacent custom file. The SDK returns a file object with a `fileId`, URL, attributes, and access type. Pass that uploaded file or its `fileId` into post, comment, or message creation APIs when you want to attach it to user content. This page covers generic files. Use the image and video pages for media-specific helpers such as image sizes, alt text, video status, and video resolution URLs. ## Platform Surface | Platform | Upload | Fetch | Delete | Notes | | --- | --- | --- | --- | --- | | TypeScript | `FileRepository.uploadFile(formData, onProgress?)` | `FileRepository.getFile(fileId)` and `getFile.locally(fileId)` | `FileRepository.deleteFile(fileId)` | `formData` must include a `files` key. Upload returns an array of uploaded files. | | iOS | `AmityFileRepository.uploadFile(_:progress:)` and `uploadFile(with:fileName:progress:)` | `getFile(fileId:)`, then `mapToFileData()` | `deleteFile(fileId:)` | URL-based upload is the source-recommended path for large local files. | | Android | `AmityCoreClient.newFileRepository().uploadFile(uri)` | `getFile(fileId)`, then `asAmityFile()` | `deleteFile(fileId)` | Upload progress is delivered through `AmityUploadResult`. | | Flutter | `AmityCoreClient.newFileRepository().uploadFile(file)` | Not exposed as a direct public file-repository fetch method in the current SDK | Not exposed as a direct public file-repository delete method in the current SDK | Use the uploaded `AmityFile` or file data loaded through post, comment, or message models. | ## Parameters | Parameter | Platforms | Description | | --- | --- | --- | | `formData` / `file` / `url` / `uri` | TypeScript, iOS, Android, Flutter | The local file input. TypeScript expects `FormData`; iOS accepts `AmityUploadableFile` or a local `URL`; Android accepts `Uri`; Flutter accepts `File`. | | `fileName` | iOS | Optional filename for URL-based upload. If omitted, the SDK derives the name from the URL. | | `fileId` | TypeScript, iOS, Android | ID returned by upload or embedded in content data. Used for direct fetch and delete calls where available. | | `onProgress` / `progress` | TypeScript, iOS, Android, Flutter | Upload progress callback or stream event. TypeScript emits a percentage; iOS emits `0.0...1.0`; Android and Flutter expose progress through `AmityUploadResult`. | | `uploadId` | Android, Flutter | Optional lower-level identifier for tracking or canceling a specific upload. The default public upload methods generate one for you. | ## Upload A File Upload a local file first, then use the returned `fileId` or file object in content creation APIs. ```typescript TypeScript import { FileRepository } from '@amityco/ts-sdk'; async function uploadFile(file: File) { const formData = new FormData(); formData.append('files', file); const { data: files } = await FileRepository.uploadFile(formData, percent => { console.log(`Upload progress: ${percent}%`); }); const uploadedFile = files[0]; return uploadedFile.fileId; } ``` ```swift iOS let fileURL = URL(fileURLWithPath: "/tmp/document.pdf") let uploadedFile = try await fileRepository.uploadFile( with: fileURL, fileName: "document.pdf", progress: { progress in print("Upload progress: \(progress)") } ) let uploadedFileId = uploadedFile.fileId ``` ```kotlin Android val fileUri = Uri.parse("file:///tmp/document.pdf") AmityCoreClient.newFileRepository() .uploadFile(fileUri) .doOnNext { result: AmityUploadResult -> when (result) { is AmityUploadResult.PROGRESS -> { val progress = result.getUploadInfo().getProgressPercentage() } is AmityUploadResult.COMPLETE -> { val uploadedFile = result.getFile() val uploadedFileId = uploadedFile.getFileId() } is AmityUploadResult.ERROR -> { val error = AmityError.from(result.getError()) } is AmityUploadResult.CANCELLED -> { // Upload was canceled. } } } .subscribe() ``` ```dart Flutter import 'dart:io'; final file = File('/tmp/document.pdf'); AmityCoreClient.newFileRepository() .uploadFile(file) .stream .listen((AmityUploadResult result) { result.when( progress: (uploadInfo, cancelToken) { final progress = uploadInfo.getProgressPercentage(); }, complete: (uploadedFile) { final uploadedFileId = uploadedFile.fileId; final uploadedFileUrl = uploadedFile.getUrl; }, error: (error) { final exception = error; }, cancel: () { // Upload was canceled. }, ); }); ``` ## Read File Data Read file data when your app needs a direct file URL, filename, or MIME type for a previously uploaded file. ```typescript TypeScript import { FileRepository } from '@amityco/ts-sdk'; async function getFile(fileId: string) { const { data: file } = await FileRepository.getFile<'file'>(fileId); return { fileId: file.fileId, fileUrl: file.fileUrl, name: file.attributes.name, mimeType: file.attributes.mimeType, }; } ``` ```swift iOS let rawFile = try await fileRepository.getFile(fileId: fileId) if rawFile.type == .file, let fileData = rawFile.mapToFileData() { let localURL = try await fileRepository.downloadFile(fromURL: fileData.fileURL) print("Downloaded file to \(localURL)") } ``` ```kotlin Android AmityCoreClient.newFileRepository() .getFile(fileId) .doOnSuccess { rawFile: AmityRawFile -> if (rawFile.getFileType() == AmityFileType.FILE) { val file = rawFile.asAmityFile() val fileUrl = file.getUrl() val fileName = file.getFileName() } } .subscribe() ``` ```dart Flutter void inspectFile(AmityFile uploadedFile) { final uploadedFileId = uploadedFile.fileId; final uploadedFileUrl = uploadedFile.getUrl; final uploadedFileName = uploadedFile.fileName; } ``` ## Delete A File Delete only files your app no longer needs. If a file is still referenced by a post, comment, message, user profile, community, or channel, update that content first so users do not see broken attachments. ```typescript TypeScript import { FileRepository } from '@amityco/ts-sdk'; async function deleteFile(fileId: string) { const { success } = await FileRepository.deleteFile(fileId); return success; } ``` ```swift iOS try await fileRepository.deleteFile(fileId: fileId) ``` ```kotlin Android AmityCoreClient.newFileRepository() .deleteFile(fileId) .subscribe() ``` ## Related Topics Upload images, read image metadata, and request sized image URLs. Upload videos and read transcoding status or resolution URLs. Attach uploaded files to social posts. --- ### [Image Handling](https://learn.social.plus/social-plus-sdk/core-concepts/content-handling/files-images-and-videos/image-handling) > Upload images, read image metadata, and request sized image URLs with the Social+ SDKs. Use image handling when your app needs to upload image files for avatars, posts, comments, messages, stories, or other media surfaces. Image uploads return image file data with a `fileId`, URL, metadata, access type, and, on supported platforms, alt text. For image posts, comments, messages, and avatars, first upload the image through the file repository, then pass the returned image object or `fileId` into the relevant creation or update API. ## Platform Surface | Platform | Upload | Fetch | Sized URLs | Alt text | | --- | --- | --- | --- | --- | | TypeScript | `FileRepository.uploadImage(formData, onProgress?, altText?)` | `FileRepository.getFile(fileId)` | `FileRepository.fileUrlWithSize(fileUrl, size)` with `small`, `medium`, `large`, `full` | Upload and `updateAltText(fileId, altText)` | | iOS | `AmityFileRepository.uploadImage(_:altText:progress:)` and `uploadImage(with:isFullImage:altText:progress:completion:)` | `getFile(fileId:)`, then `mapToImageData()` | `downloadImage(fromURL:size:)` with `AmityMediaSize.small`, `.medium`, `.large`, `.full` | Upload and `updateAltText(fileId:altText:)` | | Android | `AmityCoreClient.newFileRepository().uploadImage(uri, altText?)` | `getFile(fileId)`, then `asAmityImage()` | `AmityImage.getUrl(AmityImage.Size)` with `SMALL`, `MEDIUM`, `LARGE` | Upload and `updateAltText(fileId, altText)` | | Flutter | `AmityCoreClient.newFileRepository().uploadImage(file, isFullImage?)` | Not exposed as a direct public file-repository fetch method in the current SDK | `AmityImage.getUrl(AmityImageSize)` with `SMALL`, `MEDIUM`, `LARGE`, `FULL` | No public upload alt-text parameter in the current SDK | ## Parameters | Parameter | Platforms | Description | | --- | --- | --- | | `formData` / `image` / `url` / `uri` / `file` | TypeScript, iOS, Android, Flutter | The image input. TypeScript accepts `FormData`; iOS accepts `UIImage` or a local image `URL`; Android accepts `Uri`; Flutter accepts `File`. | | `altText` | TypeScript, iOS, Android | Optional accessibility text stored on the image file. Flutter's current public upload method does not expose this parameter. | | `isFullImage` | iOS, Flutter | Whether the uploaded image should be treated as the full image. iOS documents this on the URL-based upload; Flutter exposes `isFullImage` on public upload. | | `fileId` | TypeScript, iOS, Android | ID returned by upload or embedded in content data. Used for direct fetch, size lookup, or alt-text update where available. | | `size` | TypeScript, iOS, Android, Flutter | Requested display size. Size names differ slightly by platform, and Android currently exposes `SMALL`, `MEDIUM`, and `LARGE`. | | `onProgress` / `progress` | TypeScript, iOS, Android, Flutter | Upload progress callback or stream event. | ## Upload An Image Upload an image first, then use the returned image object or `fileId` in avatar, post, comment, message, or story APIs. ```typescript TypeScript import { FileRepository } from '@amityco/ts-sdk'; async function uploadImage(image: File) { const formData = new FormData(); formData.append('file', image); const { data: images } = await FileRepository.uploadImage( formData, percent => { console.log(`Upload progress: ${percent}%`); }, 'Profile photo', ); return images[0].fileId; } ``` ```swift iOS let image = UIImage() let uploadedImage = try await fileRepository.uploadImage( image, altText: "Profile photo", progress: { progress in print("Upload progress: \(progress)") } ) let imageFileId = uploadedImage.fileId ``` ```kotlin Android val imageUri = Uri.parse("file:///tmp/profile.jpg") AmityCoreClient.newFileRepository() .uploadImage(uri = imageUri, altText = "Profile photo") .doOnNext { result: AmityUploadResult -> when (result) { is AmityUploadResult.PROGRESS -> { val progress = result.getUploadInfo().getProgressPercentage() } is AmityUploadResult.COMPLETE -> { val uploadedImage = result.getFile() val imageFileId = uploadedImage.getFileId() } is AmityUploadResult.ERROR -> { val error = AmityError.from(result.getError()) } is AmityUploadResult.CANCELLED -> { // Upload was canceled. } } } .subscribe() ``` ```dart Flutter import 'dart:io'; final image = File('/tmp/profile.jpg'); AmityCoreClient.newFileRepository() .uploadImage(image, isFullImage: true) .stream .listen((AmityUploadResult result) { result.when( progress: (uploadInfo, cancelToken) { final progress = uploadInfo.getProgressPercentage(); }, complete: (uploadedImage) { final imageFileId = uploadedImage.fileId; final fullImageUrl = uploadedImage.getUrl(AmityImageSize.FULL); }, error: (error) { final exception = error; }, cancel: () { // Upload was canceled. }, ); }); ``` ## Read Image Data Read image data when your app needs image dimensions, alt text, or a sized image URL for rendering. ```typescript TypeScript import { FileRepository } from '@amityco/ts-sdk'; async function getImage(imageFileId: string) { const { data: image } = await FileRepository.getFile<'image'>(imageFileId); const mediumUrl = FileRepository.fileUrlWithSize(image.fileUrl, 'medium'); return { mediumUrl, altText: image.altText, width: image.attributes.metadata.width, height: image.attributes.metadata.height, }; } ``` ```swift iOS let rawFile = try await fileRepository.getFile(fileId: imageFileId) if rawFile.type == .image, let imageData = rawFile.mapToImageData() { let width = imageData.metadata["width"] as? Int let height = imageData.metadata["height"] as? Int fileRepository.downloadImage(fromURL: imageData.fileURL, size: .medium) { localURL, error in print("Downloaded image: \(String(describing: localURL))") } } ``` ```kotlin Android AmityCoreClient.newFileRepository() .getFile(imageFileId) .doOnSuccess { rawFile: AmityRawFile -> val image = rawFile.asAmityImage() val mediumUrl = image?.getUrl(AmityImage.Size.MEDIUM) val width = image?.getWidth() val height = image?.getHeight() val altText = image?.getAltText() } .subscribe() ``` ```dart Flutter void inspectImage(AmityImage image) { final mediumUrl = image.getUrl(AmityImageSize.MEDIUM); final width = image.getWidth(); final height = image.getHeight(); final isFullImage = image.isFullImage(); } ``` ## Related Topics Attach uploaded images to social posts. Attach uploaded images to comments. Upload and inspect generic file attachments. --- ### [Video Handling](https://learn.social.plus/social-plus-sdk/core-concepts/content-handling/files-images-and-videos/video-handling) > Upload videos and read transcoding status or resolution URLs with the Social+ SDKs. Use video handling when your app needs to upload video files for posts, messages, stories, clips, or other media experiences. Video uploads return a video file object with a `fileId`, original URL, and, on platforms that expose it, transcoding status and a map of generated resolution URLs. The SDK exposes upload progress and video file metadata. Playback UI, caching, retry policy, and adaptive player behavior remain application concerns. ## Platform Surface | Platform | Upload | Fetch | Status and resolutions | Notes | | --- | --- | --- | --- | --- | | TypeScript | `FileRepository.uploadVideo(formData, feedType?, onProgress?)` | `FileRepository.getFile(fileId)` | `status` and `videoUrl` on the returned file | `ContentFeedType` includes `story`, `clip`, `chat`, `post`, and `message`. | | iOS | `AmityFileRepository.uploadVideo(with:progress:)` and `uploadVideo(with:feedType:progress:completion:)` | `getFile(fileId:)`, then `mapToVideoData()` | `AmityVideoData.status`, `videoUrls`, and `getVideo(resolution:)` | The async upload method checks for files over 1 GB. The feed-type callback overload checks 4 GB and 2 hours. | | Android | `AmityCoreClient.newFileRepository().uploadVideo(uri, contentFeedType)` | `getFile(fileId)`, then `asAmityVideo()` | `getStatus()`, `getResolutions()`, and `getVideoUrl(resolution)` | `AmityContentFeedType` exposes `STORY`, `CLIP`, `MESSAGE`, and `POST`. | | Flutter | `AmityCoreClient.newFileRepository().uploadVideo(file, feedtype?)` | Not exposed as a direct public file-repository fetch method in the current SDK | `AmityVideo.getResolutions()` and `getVideoUrl(resolution)` on video objects | `AmityContentFeedType` exposes `STORY`, `POST`, `MESSAGE`, and `CLIP`. | ## Parameters | Parameter | Platforms | Description | | --- | --- | --- | | `formData` / `url` / `uri` / `file` | TypeScript, iOS, Android, Flutter | The local video input. TypeScript expects `FormData` with a `files` key; iOS accepts a local `URL`; Android accepts `Uri`; Flutter accepts `File`. | | `feedType` / `contentFeedType` / `feedtype` | TypeScript, iOS, Android, Flutter | Optional or required video context depending on platform. It tells the backend which content surface the video is for. | | `fileId` | TypeScript, iOS, Android | ID returned by upload or embedded in content data. Used for direct fetch where available. | | `resolution` | TypeScript, iOS, Android, Flutter | Requested generated video URL. Exposed values include original, 1080p, 720p, 480p, and 360p where available for that upload. | | `onProgress` / `progress` | TypeScript, iOS, Android, Flutter | Upload progress callback or stream event. | ## Upload A Video Upload a video first, then use the returned video object or `fileId` in video-based content APIs. ```typescript TypeScript import { ContentFeedType, FileRepository } from '@amityco/ts-sdk'; async function uploadVideo(video: File) { const formData = new FormData(); formData.append('files', video); const { data: videos } = await FileRepository.uploadVideo( formData, ContentFeedType.POST, percent => { console.log(`Upload progress: ${percent}%`); }, ); return videos[0].fileId; } ``` ```swift iOS let videoURL = URL(fileURLWithPath: "/tmp/video.mov") let uploadedVideo = try await fileRepository.uploadVideo( with: videoURL, progress: { progress in print("Upload progress: \(progress)") } ) let videoFileId = uploadedVideo.fileId ``` ```kotlin Android val videoUri = Uri.parse("file:///tmp/video.mp4") AmityCoreClient.newFileRepository() .uploadVideo( uri = videoUri, contentFeedType = AmityContentFeedType.POST ) .doOnNext { result: AmityUploadResult -> when (result) { is AmityUploadResult.PROGRESS -> { val progress = result.getUploadInfo().getProgressPercentage() } is AmityUploadResult.COMPLETE -> { val uploadedVideo = result.getFile() val videoFileId = uploadedVideo.getFileId() } is AmityUploadResult.ERROR -> { val error = AmityError.from(result.getError()) } is AmityUploadResult.CANCELLED -> { // Upload was canceled. } } } .subscribe() ``` ```dart Flutter import 'dart:io'; final video = File('/tmp/video.mp4'); AmityCoreClient.newFileRepository() .uploadVideo(video, feedtype: AmityContentFeedType.POST) .stream .listen((AmityUploadResult result) { result.when( progress: (uploadInfo, cancelToken) { final progress = uploadInfo.getProgressPercentage(); }, complete: (uploadedVideo) { final videoFileId = uploadedVideo.fileId; final resolutions = uploadedVideo.getResolutions(); }, error: (error) { final exception = error; }, cancel: () { // Upload was canceled. }, ); }); ``` ## Read Video Status And URLs Read video status and resolution URLs when your UI needs to render playback or processing state. ```typescript TypeScript import { FileRepository } from '@amityco/ts-sdk'; async function getVideo(videoFileId: string) { const { data: video } = await FileRepository.getFile<'video'>(videoFileId); const playbackUrl = video.videoUrl?.['720p'] ?? video.fileUrl; return { playbackUrl, status: video.status, }; } ``` ```swift iOS let rawFile = try await fileRepository.getFile(fileId: videoFileId) if rawFile.type == .video, let videoData = rawFile.mapToVideoData() { let status = videoData.status let resolutions = Array(videoData.videoUrls.keys).sorted() let playbackURL = videoData.getVideo(resolution: .res_720p) } ``` ```kotlin Android AmityCoreClient.newFileRepository() .getFile(videoFileId) .doOnSuccess { rawFile: AmityRawFile -> val video = rawFile.asAmityVideo() val status = video?.getStatus() val resolutions = video?.getResolutions().orEmpty() val playbackUrl = video?.getVideoUrl(AmityVideoResolution.RES_720) } .subscribe() ``` ```dart Flutter void inspectVideo(AmityVideo video) { final resolutions = video.getResolutions(); final playbackUrl = video.getVideoUrl(AmityVideoResolution.RES_720); } ``` ## Related Topics Attach uploaded videos to social posts. Upload and inspect generic file attachments. Create short-form clip posts where supported. --- ### [Mentions](https://learn.social.plus/social-plus-sdk/core-concepts/content-handling/mentions) > Attach user mention payloads and mention metadata when creating or editing posts, comments, and messages. Mentions are attached to content creation or edit calls. They use two pieces of data: `mentionees` tells Social+ which users or channel are mentioned, and `metadata` stores the text ranges your UI can use to render the highlighted mention text. User mentions are available on posts, comments, and messages. Channel mentions are message-only. ## Platform Surface | Platform | Mentionees payload | Mention metadata helper | | --- | --- | --- | | TypeScript | `mentionees: [{ type: "user", userIds: [...] }]` | Plain `metadata` object with `mentioned` entries | | iOS | `AmityMentioneesBuilder` | `AmityMetadataMapper.metadata(mentions:)` | | Android | `mentionUserIds` on post APIs or `mentionUsers()` on builders | `AmityMentionMetadataCreator` | | Flutter | `mentionUsers()` on builders | `AmityMentionMetadataCreator` | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `text` | Yes | Content text that includes the mention display text, such as `@alex`. | | `mentionees` / mention builder | Yes for mention behavior | Users or channel being mentioned. Posts and comments support user mentionees; messages support user and channel mentionees. | | `metadata` | Recommended for rendering | Metadata object containing mention text ranges under `mentioned`. | | `type` | Yes in metadata | Use `user` for user mentions or `channel` for message channel mentions. | | `index` | Yes in metadata | Zero-based index where the mention starts in the content text. | | `length` | Yes in metadata | Length of the display name after the `@` character. For `@alex`, use `4`. | | `userId` | User mentions only | ID of the mentioned user. | ## Create Content With A User Mention The examples below create a community text post that mentions one user. Use the same mentionee and metadata pattern when creating comments or messages with the SDK-specific creation API. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const text = "Hello @alex"; const metadata = { mentioned: [ { type: "user", userId, index: 6, length: 4, }, ], }; const { data: post } = await PostRepository.createPost({ targetType: "community", targetId: communityId, data: { text }, metadata, mentionees: [{ type: "user", userIds: [userId] }], }); ``` ```swift iOS let text = "Hello @alex" let mention = AmityMention( type: .user, index: 6, length: 4, userId: userId ) let metadata = AmityMetadataMapper.metadata(mentions: [mention]) let mentionees = AmityMentioneesBuilder() mentionees.mentionUsers(userIds: [userId]) let builder = AmityTextPostBuilder() builder.setText(text) let post = try await postRepository.createTextPost( builder, targetId: communityId, targetType: .community, metadata: metadata, mentionees: mentionees ) ``` ```kotlin Android import com.amity.socialcloud.sdk.helper.core.mention.AmityMentionMetadata import com.amity.socialcloud.sdk.helper.core.mention.AmityMentionMetadataCreator val text = "Hello @alex" val mentionMetadata = AmityMentionMetadata.USER( userId = userId, index = 6, length = 4 ) val metadata = AmityMentionMetadataCreator( mentionMetaData = listOf(mentionMetadata) ).create() postRepository.createTextPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, text = text, metadata = metadata, mentionUserIds = setOf(userId) ) .subscribe( { post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final text = 'Hello @alex'; final metadata = AmityMentionMetadataCreator([ AmityUserMentionMetadata( userId: userId, index: 6, length: 4, ), ]).create(); final creator = AmitySocialClient.newPostRepository() .createPost() .targetCommunity(communityId) .text(text); creator.mentionUsers([userId]); creator.metadata(metadata); final post = await creator.createTextPost(); ``` ## Rendering Mentions Use the content text and `metadata.mentioned` entries together when rendering. The `mentionees` payload identifies mentioned users or channel targets, while `metadata` identifies where the mention appears in the text. | Metadata field | Meaning | | --- | --- | | `type` | `user` or `channel` | | `index` | Start position of the mention in the text | | `length` | Display-name length after `@` | | `userId` | Present for user mentions | ## Notes - Keep `metadata` and `mentionees` in sync. A highlighted `@alex` without a matching mentionee payload is only display metadata. - Use the user IDs returned by your mention picker. Display names can change, but user IDs are the stable mention target. - For messages, channel mentions use a channel mentionee entry. Posts and comments use user mentionees. - Existing content models expose mention information after creation; use the model returned by the SDK or a fresh query when your UI needs server-confirmed state. ## Related Topics Create text posts that can carry mention metadata Add user mentions to comments and replies Use user and channel mentions in chat messages --- ### [Polls](https://learn.social.plus/social-plus-sdk/core-concepts/content-handling/poll) > Create polls, collect votes, and manage poll lifecycle with the Social+ SDKs. Polls are created first, then attached to poll posts when you want them to appear in a feed. The SDKs expose poll creation, voting, closing, and deletion APIs. TypeScript, iOS, and Android also expose an unvote API; the current Flutter public poll repository does not expose unvote. ## Platform Surface | Platform | Create | Vote | Unvote | Close | Delete | | --- | --- | --- | --- | --- | --- | | TypeScript | `PollRepository.createPoll()` | `votePoll()` | `unvotePoll()` | `closePoll()` | `deletePoll()` | | iOS | `AmityPollRepository.createPoll()` | `votePoll(withId:answerIds:)` | `unvotePoll(withId:)` | `closePoll(withId:)` | `deletePoll(withId:)` | | Android | `AmitySocialClient.newPollRepository().createPoll()` | `votePoll()` | `unvotePoll()` | `closePoll()` | `deletePoll()` | | Flutter | `AmitySocialClient.newPollRepository().createPoll()` | `vote()` | Not exposed | `closePoll()` | `deletePoll()` | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `question` | Yes | Poll question text. | | `answers` | Yes | Text or image answer options, depending on SDK and answer type. | | `answerType` | No | Single-choice or multiple-choice poll. Defaults to single-choice where the SDK builder provides a default. | | `closedIn` / close duration | No | Optional poll close duration. Mobile SDKs expose milliseconds or `Duration`; TypeScript passes `closedIn` through as a numeric field. | | `pollId` | Required after creation | ID returned by poll creation and used for voting, closing, deleting, and poll posts. | | `answerIds` | Required for voting | Answer IDs selected by the user. | ## Create A Poll Create the poll first and keep the returned `pollId` if you need to create a poll post. ```typescript TypeScript import { PollRepository } from "@amityco/ts-sdk"; const { data: poll } = await PollRepository.createPoll({ question: "Which feature should we build next?", answerType: "single", answers: [ { dataType: "text", data: "Bookmarks" }, { dataType: "text", data: "Pinned comments" }, ], }); const createdPollId = poll.pollId; ``` ```swift iOS let pollRepository = AmityPollRepository() let options = AmityPollCreateOptions() options.setQuestion("Which feature should we build next?") options.setAnswerType(.single) options.setAnswer("Bookmarks") options.setAnswer("Pinned comments") let createdPollId = try await pollRepository.createPoll(options) ``` ```kotlin Android import com.amity.socialcloud.sdk.model.social.poll.AmityPollAnswer val pollRepository = AmitySocialClient.newPollRepository() pollRepository.createPoll("Which feature should we build next?") .answers( listOf( AmityPollAnswer.Data.TEXT("Bookmarks"), AmityPollAnswer.Data.TEXT("Pinned comments") ) ) .answerType(AmityPoll.AnswerType.SINGLE) .build() .create() .subscribe( { createdPollId -> showSuccessMessage(createdPollId) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final poll = await AmitySocialClient.newPollRepository() .createPoll(question: 'Which feature should we build next?') .answers(answers: [ AmityPollAnswer.text('Bookmarks'), AmityPollAnswer.text('Pinned comments'), ]) .answerType(answerType: AmityPollAnswerType.SINGLE) .create(); final createdPollId = poll.pollId; ``` ## Vote And Manage A Poll Use answer IDs from the poll object when voting. Closing and deletion use the poll ID. ```typescript TypeScript import { PollRepository } from "@amityco/ts-sdk"; const answerIds = ["answer-id"]; const { data: votedPoll } = await PollRepository.votePoll(pollId, answerIds); await PollRepository.unvotePoll(pollId); const { data: closedPoll } = await PollRepository.closePoll(pollId); const isDeleted = await PollRepository.deletePoll(pollId); ``` ```swift iOS let pollRepository = AmityPollRepository() let answerIds = ["answer-id"] try await pollRepository.votePoll(withId: pollId, answerIds: answerIds) try await pollRepository.unvotePoll(withId: pollId) try await pollRepository.closePoll(withId: pollId) try await pollRepository.deletePoll(withId: pollId) ``` ```kotlin Android val pollRepository = AmitySocialClient.newPollRepository() val answerIds = listOf("answer-id") pollRepository.votePoll(pollId, answerIds) .subscribe( { showSuccessMessage("Vote submitted") }, { error -> handleGeneralError(error) } ) pollRepository.unvotePoll(pollId) .subscribe( { showSuccessMessage("Vote removed") }, { error -> handleGeneralError(error) } ) pollRepository.closePoll(pollId) .subscribe( { showSuccessMessage("Poll closed") }, { error -> handleGeneralError(error) } ) pollRepository.deletePoll(pollId) .subscribe( { showSuccessMessage("Poll deleted") }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final pollRepository = AmitySocialClient.newPollRepository(); final answerIds = ['answer-id']; await pollRepository.vote(pollId: pollId, answerIds: answerIds); final closedPoll = await pollRepository.closePoll(pollId: pollId); final isDeleted = await pollRepository.deletePoll(pollId: pollId); ``` ## Notes - Create a poll post separately after poll creation. Poll creation returns a poll ID; poll-post creation attaches that ID to feed content. - Use `answerType` values that match each SDK: TypeScript uses `single` or `multiple`, iOS uses `.single` or `.multiple`, Android uses `AmityPoll.AnswerType`, and Flutter uses `AmityPollAnswerType`. - Flutter currently exposes `vote`, `closePoll`, and `deletePoll`, but not an unvote method on the public poll repository. - Deletion and close behavior depends on server-side permissions for the current user. ## Related Topics Attach an existing poll to a feed post Create regular text posts for the same feed targets Fetch posts that contain poll data --- ### [Reactions](https://learn.social.plus/social-plus-sdk/core-concepts/content-handling/reactions) > Query, add, and remove reactions on posts, comments, stories, and messages with the Social+ SDKs. Reactions let signed-in users attach a named reaction such as `like` to content. The SDKs expose the same core shape on every platform: choose a reference type, pass the content ID, and pass the reaction name. ## Platform Surface | Platform | Query reactions | Add reaction | Remove reaction | | --- | --- | --- | --- | | TypeScript | `ReactionRepository.getReactions()` | `ReactionRepository.addReaction()` | `ReactionRepository.removeReaction()` | | iOS | `AmityReactionRepository.getReactions()` | `AmityReactionRepository.addReaction()` | `AmityReactionRepository.removeReaction()` | | Android | `AmityCoreClient.newReactionRepository().getReactions()` | `addReaction()` | `removeReaction()` | | Flutter | `AmitySocialClient.newReactionRepository().getReactions()` | `addReaction()` | `removeReaction()` | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `referenceType` | Yes | Content type being reacted to. Supported values are post, comment, story, and message. | | `referenceId` | Yes | ID of the post, comment, story, or message. | | `reactionName` | Yes for add/remove, optional for query | Reaction key such as `like`. Query APIs can use it as a filter. | | `callback` / live collection observer | Query only | Used by live collection APIs to receive reaction updates. | ## Query Reactions Use `reactionName` when the UI only needs one reaction type. Omit it to read all reaction types for the content. ```typescript TypeScript import { ReactionRepository } from "@amityco/ts-sdk"; const unsubscribe = ReactionRepository.getReactions( { referenceType: "post", referenceId: postId, reactionName: "like", }, ({ data }) => { renderResults(data); } ); ``` ```swift iOS let reactionRepository = AmityReactionRepository() let reactions = reactionRepository.getReactions( postId, referenceType: .post, reactionName: "like" ) token = reactions.observe { collection, error in guard error == nil else { return } let currentReactions = collection.snapshots showSuccessMessage(currentReactions.count) } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.core.reaction.AmityReactionReferenceType AmityCoreClient.newReactionRepository() .getReactions( referenceType = AmityReactionReferenceType.POST, referenceId = postId, reactionName = "like" ) .subscribe( { reactions -> showSuccessMessage(reactions) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final reactions = AmitySocialClient.newReactionRepository() .getReactions(AmityPostReactionReference(referenceId: postId)) .reactionName('like') .getLiveCollection(); ``` ## Add And Remove Reactions Adding or removing a reaction returns completion or success from the SDK call. Keep UI counts synced from the content model or the reaction live collection. ```typescript TypeScript import { ReactionRepository } from "@amityco/ts-sdk"; await ReactionRepository.addReaction("post", postId, "like"); await ReactionRepository.removeReaction("post", postId, "like"); ``` ```swift iOS let reactionRepository = AmityReactionRepository() try await reactionRepository.addReaction( "like", referenceId: postId, referenceType: .post ) try await reactionRepository.removeReaction( "like", referenceId: postId, referenceType: .post ) ``` ```kotlin Android import com.amity.socialcloud.sdk.model.core.reaction.AmityReactionReferenceType val reactionRepository = AmityCoreClient.newReactionRepository() reactionRepository.addReaction( referenceType = AmityReactionReferenceType.POST, referenceId = postId, reactionName = "like" ) .subscribe( { showSuccessMessage("Reaction added") }, { error -> handleGeneralError(error) } ) reactionRepository.removeReaction( referenceType = AmityReactionReferenceType.POST, referenceId = postId, reactionName = "like" ) .subscribe( { showSuccessMessage("Reaction removed") }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final reactionRepository = AmitySocialClient.newReactionRepository(); final reference = AmityPostReactionReference(referenceId: postId); await reactionRepository.addReaction(reference, 'like'); await reactionRepository.removeReaction(reference, 'like'); ``` ## Notes - TypeScript accepts `post`, `comment`, `story`, and `message` as reaction reference types. - iOS, Android, and Flutter expose equivalent enum or reference classes for post, comment, story, and message. - Message reactions use the message reaction path in the SDKs. Do not use post or comment reference types for messages. - Reaction names are application-defined strings. Use the same values when adding, removing, and filtering. ## Related Topics React to comments and keep comment threads interactive Use the same reaction concept with story content Add and remove reactions on chat messages --- ### [Ads](https://learn.social.plus/social-plus-sdk/core-concepts/content-handling/ads) > Fetch configured network ads and track ad impressions or link clicks with the Social+ SDKs. Network ads are fetched through the core ad repository. The returned ad objects include display fields, linked advertiser or image data where available, and analytics methods for impression and click tracking. ## Platform Surface | Platform | Fetch network ads | Track seen | Track clicked | | --- | --- | --- | --- | | TypeScript | `AdRepository.getNetworkAds()` | `ad.analytics.markAsSeen()` | `ad.analytics.markLinkAsClicked()` | | iOS | `AmityAdRepository.getNetworkAds()` | `ad.analytics.markAsSeen()` | `ad.analytics.markLinkAsClicked()` | | Android | `AmityCoreClient.newAdRepository().getNetworkAds()` | `adRepository.analytics(ad).markAsSeen()` | `adRepository.analytics(ad).markLinkAsClicked()` | | Flutter | `AmityCoreClient.newAdRepository().getNetworkAds()` | `adRepository.analytics(ad).markAsSeen()` | `adRepository.analytics(ad).markLinkAsClicked()` | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `ad` | Tracking only | Ad object returned from the network ads response. | | `placement` | Tracking only | Where the ad was displayed. SDK enum values include feed, story, comment, chat, and chat list placements. | | `settings` | No | Network ad settings returned with the ad list. Use them to decide how your app presents ads. | ## Fetch And Track Ads Call the tracking methods when the ad is actually shown or when the ad link is clicked in your UI. ```typescript TypeScript import { AdRepository } from "@amityco/ts-sdk"; const networkAds = await AdRepository.getNetworkAds(); const ad = networkAds.ads[0]; if (ad) { const placement = Amity.AdPlacement.FEED; ad.analytics.markAsSeen(placement); ad.analytics.markLinkAsClicked(placement); } ``` ```swift iOS let adRepository = AmityAdRepository() let networkAds = try await adRepository.getNetworkAds() if let ad = networkAds.ads.first { ad.analytics.markAsSeen(placement: .feed) ad.analytics.markLinkAsClicked(placement: .feed) } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.core.ad.AmityAdPlacement val adRepository = AmityCoreClient.newAdRepository() adRepository.getNetworkAds() .subscribe( { networkAds -> val ad = networkAds.getAds().firstOrNull() if (ad != null) { adRepository.analytics(ad).markAsSeen(AmityAdPlacement.FEED) adRepository.analytics(ad).markLinkAsClicked(AmityAdPlacement.FEED) } }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final adRepository = AmityCoreClient.newAdRepository(); final networkAds = await adRepository.getNetworkAds(); final ads = networkAds.getAds() ?? []; if (ads.isNotEmpty) { final ad = ads.first; await adRepository.analytics(ad).markAsSeen(AmityAdPlacement.FEED); await adRepository.analytics(ad).markLinkAsClicked(AmityAdPlacement.FEED); } ``` ## Notes - Fetching ads returns both `ads` and `settings`. Use settings as display configuration from the SDK response. - Track `markAsSeen` only after the ad is rendered in the placement you pass. - Track `markLinkAsClicked` from the UI action that opens or handles the ad call-to-action link. - Use the placement enum from each SDK instead of hardcoded placement strings. ## Related Topics Place feed ads alongside feed content Use story placement when ads are displayed in stories Coordinate ad placement with chat surfaces ## Core Concepts — Realtime ### [Live Objects & Collections](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/overview) > Understand how the Social+ SDKs expose live objects and live collections for cache-backed, real-time data. Live objects and live collections are SDK patterns for reading data that can change after your first request. They let your app subscribe to SDK-managed state instead of treating every query as a one-time network response. Use a live object when the screen is centered on one entity, such as a post, message, channel, community, user, room, stream, poll, or story. Use a live collection when the screen renders a list, such as posts, comments, messages, members, followers, reactions, rooms, or streams. ## Platform Surface | Platform | Live object | Live collection | Pagination | Cleanup | | --- | --- | --- | --- | --- | | TypeScript | `Amity.LiveObject` delivered to repository callbacks such as `PostRepository.getPost()` | `Amity.LiveCollection` delivered to query callbacks such as `PostRepository.getPosts()` | `onNextPage`, `hasNextPage`, `onPrevPage`, `hasPrevPage` when supported by that query | Call the returned `Amity.Unsubscriber`; also unsubscribe from any real-time topics you subscribed to | | iOS | `AmityObject` | `AmityCollection` | `nextPage()`, `previousPage()`, `hasNext`, `hasPrevious`, `resetPage()` | Retain the returned `AmityNotificationToken`; call `invalidate()` or release the token | | Android | `Flowable` from repository methods such as `getPost(postId)` | `Flowable>` or `Flowable>`, depending on the query | Android Paging 3 through `PagingData` for paged collections | Dispose the RxJava subscription or cancel the coroutine collector | | Flutter | `Stream` from live-object builders such as `live.getPost(postId)` | `LiveCollection` / `LiveCollectionStream` | `loadNext()`, `loadPrevious()`, `hasNextPage()`, `hasPreviousPage()`, `reset()` | Cancel stream subscriptions and call `dispose()` on live collections | ## Data Sources Live results can be updated by more than one source: | Source | What it means | | --- | --- | | Local cache | The SDK may emit cached data first so the UI can render quickly. | | Server fetch | The SDK fetches newer data and updates the same live object or live collection. | | Local mutation | Actions from the current device update the local SDK store and notify observers. | | Real-time event | Events from subscribed topics update the SDK store and notify observers. | Live objects and collections observe SDK state. They do not remove the need to subscribe to the right real-time topics where a platform exposes explicit topic subscriptions. ## Live Object A live object tracks one entity. The SDK can emit: | State | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Current data | `snapshot.data` | `liveObject.snapshot` | emitted `T` value | emitted `T` value | | Loading | `snapshot.loading` | `loadingStatus` | stream lifecycle / app state | stream lifecycle / app state | | Error | `snapshot.error` | `error` or observer `error` parameter | `onError` | stream `onError` | | Freshness/origin | `origin` | `dataStatus` | not exposed as a shared public enum | not exposed as a shared public enum | Typical screens: post detail, message detail, channel profile, community header, user profile, room detail, or stream detail. ## Live Collection A live collection tracks a list. The SDK can append, refresh, or re-emit the list when local cache, server fetches, or real-time events change the underlying data. | Capability | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Current list | `collection.data` | `collection.snapshots` | `PagingData` or `List` emission | `LiveResult.data` | | Loading | `collection.loading` | `loadingStatus` | paging load state / Rx lifecycle | `LiveResult.isFetching` and `observeLoadingState()` | | Next page | `collection.onNextPage?.()` | `collection.nextPage()` | collect more through Paging 3 | `collection.loadNext()` | | Reset | Recreate the query or collection | `collection.resetPage()` | invalidate/recreate the paging source or query | `collection.reset()` | Typical screens: post feeds, comment threads, chat messages, channel lists, member lists, reaction lists, follower lists, rooms, streams, and notifications. ## Usage Notes - Keep the live subscription for as long as the UI needs updates. - Clean up subscriptions when the screen, component, view model, or widget is disposed. - Treat the first emission as potentially local or loading unless the platform exposes freshness status and it says otherwise. - Do not assume all platforms expose identical states. Use each platform's native live primitive. - For TypeScript, subscribe to the relevant topic when you need cross-device real-time updates, then dispose both the repository observer and the topic subscription. ## Platform Guides Observe live objects and collections with callbacks, pagination helpers, and topic subscriptions. Use `AmityObject`, `AmityCollection`, notification tokens, and published snapshots. Use RxJava `Flowable`, Android Paging 3, and the coroutine `asFlow()` bridge. Use Dart streams, `LiveCollection`, `LiveResult`, and collection disposal. ## Related Topics Learn how real-time events update SDK state. Track user, channel, and room presence. --- ### [Android Live Objects & Collections](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/android) > Observe Social+ Android SDK objects and collections with RxJava Flowable, Android Paging, and coroutine flows. The Android SDK exposes live objects and collections through RxJava 3 streams. Singular reads such as `getPost(postId)` return `Flowable`. Paged list queries return `Flowable>`. Some finite list queries return `Flowable>`. Use RxJava directly, or convert supported streams to Kotlin `Flow` with the SDK coroutine bridge. ## Platform Surface | Surface | Public shape | Notes | | --- | --- | --- | | Live object | `Flowable` | Repository methods such as `AmitySocialClient.newPostRepository().getPost(postId)`. | | Paged live collection | `Flowable>` | Query builders such as `getPosts().targetCommunity(...).build().query()`. | | Finite live collection | `Flowable>` | Used by selected APIs such as get-by-IDs style queries. | | Coroutine bridge | `Flowable.asFlow()` | Import `com.amity.socialcloud.sdk.helper.core.coroutines.asFlow`. | | Cleanup | `Disposable.dispose()` or coroutine cancellation | Dispose or cancel when the Activity, Fragment, ViewModel, or UI scope no longer needs updates. | ## Parameters | Parameter | Used by | Description | | --- | --- | --- | | `postId` or another entity ID | Live object methods | ID of the object to observe. | | Query builder target | Live collection builders | Scope for the collection, such as `targetCommunity(communityId)` or `targetUser(userId)`. | | `PagingData` | Paged collections | The emitted paging payload consumed by RecyclerView, Paging 3, or Compose paging. | | `subscribeOn(Schedulers.io())` | RxJava streams | Runs SDK work on an IO scheduler. | | `observeOn(AndroidSchedulers.mainThread())` | UI observers | Delivers callbacks on the Android main thread for UI updates. | ## Observe A Live Object Observe a live object when your Android UI needs one SDK object to stay current after server or local-store changes. ```kotlin Android val disposable = AmitySocialClient.newPostRepository() .getPost(postId) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { post: AmityPost -> showSuccessMessage(post.getPostId()) }, { error: Throwable -> handleGeneralError(error) } ) disposable.dispose() ``` ## Observe A Paged Live Collection The SDK query emits `PagingData`. Feed it into your Paging 3 adapter or Compose paging collector. ```kotlin Android val disposable = AmitySocialClient.newPostRepository() .getPosts() .targetCommunity(communityId) .includeDeleted(false) .build() .query() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error: Throwable -> handleGeneralError(error) } ) disposable.dispose() ``` ## Convert to Kotlin Flow Use the coroutine bridge when the rest of your app is built around Kotlin Flow. ```kotlin Android import com.amity.socialcloud.sdk.helper.core.coroutines.asFlow val postFlow = AmitySocialClient.newPostRepository() .getPost(postId) .asFlow() ``` ## Notes - A live object fetches the server object and then observes the SDK local store. The internal live-object use case also checks tombstones for hard-deleted objects. - Paged live collections use Android Paging 3, so page loading and UI load states should be handled through your Paging adapter or Compose paging integration. - Dispose RxJava subscriptions in the matching lifecycle owner or ViewModel. For coroutines, cancel the collecting scope. - Use `subscribeOn(Schedulers.io())` for SDK work and `observeOn(AndroidSchedulers.mainThread())` before mutating UI state. ## Related Topics See post-specific Android query examples. Configure Android notification setup alongside real-time data. --- ### [Flutter Live Objects & Collections](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/flutter) > Observe Social+ Flutter SDK objects and collections with Dart streams, LiveCollection, and LiveResult. The Flutter SDK exposes live objects as Dart streams and live collections through `LiveCollection` / `LiveCollectionStream`. A live collection emits `LiveResult`, which contains the current `data` list and an `isFetching` flag. ## Platform Surface | Surface | Public shape | Notes | | --- | --- | --- | | Live object | `Stream` | Example: `AmitySocialClient.newPostRepository().live.getPost(postId)`. | | Live collection | `LiveCollection` | Returned by builders such as `getPosts().targetCommunity(...).getLiveCollection()`. | | Collection stream | `Stream>` | Access with `liveCollection.getStream()`. | | Loading state | `LiveResult.isFetching` or `observeLoadingState()` | Use this to drive loading indicators. | | Pagination | `loadNext()`, `loadPrevious()`, `hasNextPage()`, `hasPreviousPage()`, `reset()` | `getStream()` also triggers the first load. | | Cleanup | `StreamSubscription.cancel()` and `liveCollection.dispose()` | Cancel listeners and dispose the live collection when the widget or bloc is done. | ## Parameters | Parameter | Used by | Description | | --- | --- | --- | | `postId` or another entity ID | Live object streams | ID of the object to observe. | | Query builder target | Live collections | Scope for the collection, such as `targetCommunity(communityId)` or `targetUser(userId)`. | | `pageSize` | Live collections | Optional page size for `getLiveCollection(pageSize: ...)`. Defaults to 20 when omitted. | | `LiveResult.data` | Collection stream | Current list snapshot. | | `LiveResult.isFetching` | Collection stream | Whether the collection is currently fetching. | ## Observe A Live Object Observe a live object when your Flutter UI needs one SDK object to stay current after server or local-cache changes. ```dart Flutter final subscription = AmitySocialClient.newPostRepository() .live .getPost(postId) .listen((post) { final currentPostId = post.postId; showError(currentPostId ?? postId); }); await subscription.cancel(); ``` ## Observe A Live Collection `getStream()` emits `LiveResult`. Keep the collection instance so you can load more pages and dispose it later. ```dart Flutter final liveCollection = AmitySocialClient.newPostRepository() .getPosts() .targetCommunity(communityId) .getLiveCollection(pageSize: 20); liveCollection.onError((error, stackTrace) { showError(error ?? stackTrace); }); final subscription = liveCollection.getStream().listen((result) { final posts = result.data; final isFetching = result.isFetching; showError('${posts.length}:$isFetching'); }); if (liveCollection.hasNextPage()) { await liveCollection.loadNext(); } await subscription.cancel(); await liveCollection.dispose(); ``` ## Notes - `getStream()` starts the first page load for `LiveCollection`. - Use `observeLoadingState()` when you only need loading changes. - Call `loadNext()` only when `hasNextPage()` is true. - Call `reset()` to clear the collection and reload the first page. - Cancel every `StreamSubscription` and call `dispose()` on live collections that are no longer used. ## Related Topics See post-specific Flutter retrieval examples. Configure Flutter notification setup alongside real-time data. --- ### [iOS Live Objects & Collections](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/ios) > Observe Social+ iOS SDK objects and collections with AmityObject, AmityCollection, notification tokens, and published snapshots. The iOS SDK exposes live objects with `AmityObject` and live collections with `AmityCollection`. Both are `ObservableObject`s and expose published state for SwiftUI or Combine. Block observation is tied to `AmityNotificationToken`; retain the token while you need updates. ## Platform Surface | Surface | Public API | Notes | | --- | --- | --- | | Live object | `AmityObject` | Exposes `snapshot`, `dataStatus`, `loadingStatus`, and `error`. | | Live collection | `AmityCollection` | Exposes `snapshots`, `dataStatus`, `loadingStatus`, and `error`. | | Block observer | `observe { ... }` | Can emit multiple times while the token is retained. | | One-time observer | `observeOnce { ... }` | Invalidates after one notification. | | Cleanup | `AmityNotificationToken.invalidate()` | Releasing the token also ends observation. | | Collection pagination | `nextPage()`, `previousPage()`, `resetPage()` | Check `hasNext` or `hasPrevious` before requesting more pages. | ## Parameters And State | Name | Applies to | Description | | --- | --- | --- | | `AmityNotificationToken` | Object and collection observers | Retain it strongly for the lifetime of the observation. Invalidate it when the view no longer needs updates. | | `snapshot` | `AmityObject` | The current object snapshot, or `nil` if the object is not available. | | `snapshots` | `AmityCollection` | The current collection snapshot array. | | `dataStatus` | Object and collection | `notExist`, `local`, `fresh`, or `error`. Use this when freshness matters. | | `loadingStatus` | Object and collection | `notLoading`, `loading`, `loaded`, or `error`. | | `hasNext` / `hasPrevious` | Collection | Indicates whether another page can be requested. | ## Observe A Live Object This example observes one post. Keep `token` in view-controller, view-model, or view scope; invalidate it when the screen disappears. ```swift iOS let livePost = postRepository.getPost(withId: postId) token = livePost.observe { observedPost, error in if let error = error { handleError(error) return } guard let post = observedPost.snapshot else { return } showSuccessMessage(post.postId) } func stopObservingPost() { token?.invalidate() token = nil } ``` ## Observe A Live Collection `AmityCollection` emits through `snapshots`. Use pagination methods on the collection instance, not array indexing helpers from older SDK generations. ```swift iOS let options = AmityPostQueryOptions( targetType: .community, targetId: communityId, sortBy: .lastCreated, deletedOption: .notDeleted, dataTypes: nil ) let livePosts = postRepository.getPosts(options) token = livePosts.observe { collection, error in if let error = error { handleError(error) return } let posts = collection.snapshots let isFresh = collection.dataStatus == .fresh showSuccessMessage(posts.count) showSuccessMessage(isFresh) } if livePosts.hasNext { livePosts.nextPage() } ``` ## Observe Published Snapshots `AmityObject` and `AmityCollection` are also observable from SwiftUI or Combine through published properties. ```swift iOS var cancellable: AnyCancellable? let livePost = postRepository.getPost(withId: postId) cancellable = livePost.$snapshot.sink { post in guard let post = post else { return } showSuccessMessage(post.postId) } ``` ## Notes - Current iOS SDK live objects expose `snapshot`; live collections expose `snapshots`. - Do not use older `object`, `object(at:)`, or `count()` patterns with current live objects and collections. - Observer callbacks are dispatched on the main thread. - If an object has local data, the SDK can emit local state before fresh server state. - For fresh-only flows, wait until `dataStatus == .fresh`, then invalidate the token if you do not need future updates. - For SwiftUI, pass the live object or collection directly into the view that observes it. Nested `ObservableObject` containers can hide changes from SwiftUI. ## Related Topics See post-specific iOS retrieval examples. Learn how server events keep SDK state fresh. --- ### [TypeScript Live Objects & Collections](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/live-objects-collections/typescript) > Observe Social+ TypeScript SDK objects and collections with callbacks, pagination helpers, and real-time topic subscriptions. The TypeScript SDK exposes live objects and live collections through repository callbacks. A live object callback receives one object snapshot. A live collection callback receives a list snapshot plus pagination helpers when the query supports paging. Repository observers watch SDK cache state. For cross-device real-time updates, subscribe to the topic that matches the object or collection scope, then clean up both the repository observer and the topic subscription. ## Platform Surface | Surface | Public shape | Notes | | --- | --- | --- | | Live object | `Amity.LiveObject` | Callback payload includes `data`, `loading`, `error`, and optional `origin`. | | Live collection | `Amity.LiveCollection` | Extends the live-object shape with `onNextPage`, `hasNextPage`, `onPrevPage`, and `hasPrevPage` where available. | | Observer cleanup | `Amity.Unsubscriber` | Repository live methods return an unsubscribe function. | | Real-time topic cleanup | `Amity.Unsubscriber` | `subscribeTopic()` returns a separate unsubscribe function. | | Collection config | `Amity.LiveCollectionConfig` | Supports query policies except `no_fetch`. | ## Parameters | Parameter | Used by | Description | | --- | --- | --- | | `postId` or another entity ID | Live object methods | ID of the object to observe, such as a post, message, channel, community, user, poll, room, or stream. | | `params.targetType` / `params.targetId` | Collection queries such as `PostRepository.getPosts()` | Scope for the list. For posts, `targetType` is commonly `user` or `community`. | | `limit` | Collection queries | Optional page size for the query. | | `callback` | Object and collection methods | Receives each live snapshot. Handle `loading` and `error` before rendering data. | | `onNextPage` / `hasNextPage` | Live collections | Use when the UI asks for the next page. Do not call `onNextPage` repeatedly without checking `hasNextPage`. | | `subscribeTopic(topic, callback?)` | Real-time updates | Subscribes the active client to a topic such as `getPostTopic(post)` or `getCommunityTopic(community, SubscriptionLevels.POST)`. | ## Observe A Live Object This example observes one post and subscribes to the post topic after the first post snapshot is available. ```typescript TypeScript import { PostRepository, getPostTopic, subscribeTopic } from "@amityco/ts-sdk"; let unsubscribePostTopic: Amity.Unsubscriber | undefined; const unsubscribePost = PostRepository.getPost(postId, snapshot => { if (snapshot.loading) { showLoading(); } if (snapshot.error) { handleError(snapshot.error); return; } const currentPost = snapshot.data; renderResults(currentPost); if (!unsubscribePostTopic) { unsubscribePostTopic = subscribeTopic(getPostTopic(currentPost), error => { if (error) handleError(error); }); } }); function stopObservingPost() { unsubscribePost(); unsubscribePostTopic?.(); } ``` ## Observe A Live Collection Use the collection callback to render the latest list. Keep the next-page function and call it only when the UI requests more data. ```typescript TypeScript import { PostRepository, SubscriptionLevels, getCommunityTopic, subscribeTopic, } from "@amityco/ts-sdk"; let hasMorePosts = false; let loadNextPostsPage: (() => void) | undefined; const unsubscribePostsTopic = subscribeTopic( getCommunityTopic(community, SubscriptionLevels.POST), error => { if (error) handleError(error); } ); const unsubscribePosts = PostRepository.getPosts( { targetType: "community", targetId: communityId, limit: 20, sortBy: "lastCreated", includeDeleted: false, }, collection => { if (collection.loading) { showLoading(); } if (collection.error) { handleError(collection.error); return; } renderResults(collection.data); hasMorePosts = collection.hasNextPage ?? false; loadNextPostsPage = collection.onNextPage; } ); function loadMorePosts() { if (hasMorePosts) { loadNextPostsPage?.(); } } function stopObservingPosts() { unsubscribePosts(); unsubscribePostsTopic(); } ``` ## Notes - `getPost()`, `getMessage()`, `getChannel()`, `getCommunity()`, `getUser()`, `getPoll()`, `getRoom()`, and similar singular methods follow the same live-object callback pattern. - Query methods such as `getPosts()`, `getComments()`, `getMessages()`, `getMembers()`, `getReactions()`, `getRooms()`, and `getStreams()` follow the live-collection callback pattern. - The callback can emit local/cache data before server data. Use `loading`, `error`, and `origin` if your UI needs to distinguish phases. - Keep topic subscriptions as narrow as possible. A post-detail screen should subscribe to the post topic; a community feed screen should subscribe to the community post topic. - Always dispose the observer returned by the repository and any topic unsubscribe functions returned by `subscribeTopic()`. ## Related Topics See post-specific live object and collection usage. Subscribe to social topics for cross-device updates. --- ### [Real-time Events](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/realtime-events/overview) > Understand how SDK topic subscriptions deliver social and chat updates into live objects and collections. Real-time events are MQTT topic subscriptions managed by the SDK. Subscribe to the smallest topic that matches the screen, and the SDK delivers matching changes into the live objects, live collections, and local caches you already observe. Topic subscriptions do not replace Live Objects & Collections. Use subscriptions to tell the SDK which event streams matter now, then render from the live data APIs. ## Platform Surface | Area | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Social topics | Topic helper functions such as `getCommunityTopic`, `getPostTopic`, and `subscribeTopic` | `AmityTopic` classes with `AmityTopicSubscription` | Model `.subscription(...)` extensions plus `AmityCoreClient.subscription(...)` for follow topics | Model `.subscription(...)` extensions | | Chat topics | `getSubChannelTopic(subChannel)` with `subscribeTopic(topic)` | `AmitySubChannelTopic` with `AmityTopicSubscription` | `subChannel.subscription()` | `subChannel.subscription()` | | Unsubscribe | Call the `Amity.Unsubscriber` returned by `subscribeTopic` | `unsubscribeTopic(...)` | `unsubscribeTopic()` | `unsubscribeTopic()` | ## Topic Lifecycle ```mermaid flowchart LR A["Screen needs live updates"] --> B["Choose model topic"] B --> C["Subscribe while visible"] C --> D["Live object or collection receives updates"] D --> E["Unsubscribe when no longer needed"] ``` ## When to Subscribe | UI need | Recommended topic | | --- | --- | | Community profile or settings changes | Community topic with community-level events | | Community feed creation, deletion, or updates | Community topic with post events, or post-and-comment events for a full feed | | A single post detail screen | Post topic for the post, and comment events if comments are visible | | A comment detail or moderation surface | Comment topic | | A user profile screen | User topic with user-level events | | A chat thread | Subchannel topic | | Follow/follower list changes | Follow topic on platforms that expose follow topic helpers | | Story updates | Story topic or community-story topic where available | ## Subscription Discipline Topic APIs usually require an SDK model object such as `AmityCommunity`, `AmityPost`, `AmityComment`, `AmityUser`, or `AmitySubChannel`. Fetch or observe the model first, then subscribe while that model is visible. A community feed usually needs one community post-and-comment topic, not a separate topic for every post row. Use post or comment topics for focused detail screens. Unsubscribe when the screen is dismissed, the component unmounts, or the observed model changes. Logout also tears down the SDK session, but UI-level cleanup keeps each screen's ownership clear. A topic subscription activates event delivery. Your UI should still read from the SDK live object or live collection so cache updates, loading state, and error handling stay consistent. ## Related Topics Subscribe to community, post, comment, user, follow, and story topics. Subscribe to subchannel topics for chat thread updates. Observe SDK-managed live data after subscribing to the event streams your UI needs. --- ### [Chat Real-time Events](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/realtime-events/chat-realtime-events) > Subscribe to subchannel topics for live chat thread updates. Chat real-time event subscriptions keep a thread's live data moving while the user is viewing it. For the current SDK surface, the public chat topic documented here is the subchannel topic. Conversation and community channel members receive many chat updates through normal SDK live objects and collections. Use a subchannel topic when the screen needs explicit realtime delivery for a specific message thread. ## Platform Surface | Platform | Public API | Cleanup | | --- | --- | --- | | TypeScript | `getSubChannelTopic(subChannel)`, then `subscribeTopic(topic)` | Call the returned `Amity.Unsubscriber` | | iOS | `AmitySubChannelTopic(subChannel:)`, then `AmityTopicSubscription().subscribeTopic(...)` | `AmityTopicSubscription().unsubscribeTopic(...)` | | Android | `subChannel.subscription().subscribeTopic()` | `subChannel.subscription().unsubscribeTopic()` | | Flutter | `subChannel.subscription().subscribeTopic()` | `subChannel.subscription().unsubscribeTopic()` | ## Parameters | Platform | Parameter | Required | Description | | --- | --- | --- | --- | | TypeScript | `subChannel` | Yes | `Amity.SubChannel` model returned by the SDK. The helper reads its `path` and subscribes to that path with a wildcard. | | iOS | `subChannel` | Yes | `AmitySubChannel` model used to construct `AmitySubChannelTopic`. | | Android | `subChannel` | Yes | `AmitySubChannel` model exposing the public `subscription()` method. | | Flutter | `subChannel` | Yes | `AmitySubChannel` model exposing the public `subscription()` method. | ## Subscribe To A Subchannel Subscribe when the thread screen becomes active, and unsubscribe when it leaves the screen or switches to another subchannel. ```typescript TypeScript import { getSubChannelTopic, subscribeTopic } from '@amityco/ts-sdk'; function subscribeToSubChannel(subChannel: Amity.SubChannel): Amity.Unsubscriber { const topic = getSubChannelTopic(subChannel); return subscribeTopic(topic); } ``` ```swift iOS func subscribeToSubChannel(_ subChannel: AmitySubChannel) async throws { let topic = AmitySubChannelTopic(subChannel: subChannel) try await AmityTopicSubscription().subscribeTopic(topic) } func unsubscribeFromSubChannel(_ subChannel: AmitySubChannel) async throws { let topic = AmitySubChannelTopic(subChannel: subChannel) try await AmityTopicSubscription().unsubscribeTopic(topic) } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.chat.subchannel.AmitySubChannel fun subscribeToSubChannel(subChannel: AmitySubChannel) { subChannel.subscription() .subscribeTopic() .subscribe({ showSuccessMessage("Subscribed") }, { error -> showErrorMessage(error = error) }) } fun unsubscribeFromSubChannel(subChannel: AmitySubChannel) { subChannel.subscription() .unsubscribeTopic() .subscribe() } ``` ```dart Flutter Future subscribeToSubChannel(AmitySubChannel subChannel) async { await subChannel.subscription().subscribeTopic(); } Future unsubscribeFromSubChannel(AmitySubChannel subChannel) async { await subChannel.subscription().unsubscribeTopic(); } ``` ## Best Practices Bind the subchannel subscription to the active message thread, not to the entire chat module. Switch the subscription when the user changes threads. The snippets expect a real `SubChannel` model returned by the SDK. Avoid constructing topic paths manually in app code. After subscribing, render messages from the SDK query or live collection APIs so local cache updates and pagination stay consistent. ## Related Topics Query and render messages for a subchannel. Understand how realtime events update observed SDK data. --- ### [Social Real-time Events](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/realtime-events/social-realtime-events) > Subscribe to community, post, comment, user, follow, and story topic events. Social real-time events let an app receive live updates for the social models that are currently visible. Use topic subscriptions for the event stream, then render from the SDK live object or live collection for the model. Community and user-scoped topics are useful for feed screens. Post and comment topics are better for focused detail screens. The user must have access to the target community, post, comment, user, or story for events to be delivered. ## Platform Surface | Topic | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Community | `getCommunityTopic(...)` | `AmityCommunityTopic` | `community.subscription(...)` | `community.subscription(...)` | | Post | `getPostTopic(...)`, `getLiveReactionTopic(...)` | `AmityPostTopic` | `post.subscription(...)` | `post.subscription(...)` | | Comment | `getCommentTopic(...)` | `AmityCommentTopic` | `comment.subscription(...)` | `comment.subscription(...)` | | User | `getUserTopic(...)` | `AmityUserTopic` | `user.subscription(...)` | `user.subscription(...)` | | Follow | `getMyFollowersTopic()`, `getMyFollowingsTopic()` | `AmityFollowTopic` | `AmityCoreClient.subscription(AmityTopic.FOLLOW(...))` | Not exposed in this checkout | | Story | `getStoryTopic(...)`, `getCommunityStoriesTopic(...)` | `AmityStoryTopic` | `story.subscription()` | `story.subscription()` | ## Community Topic Use a community topic when a screen is scoped to a community, such as a community profile, feed, or moderation surface. ### Event Levels | Intent | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Community object changes | `SubscriptionLevels.COMMUNITY` or omit the level | `.community` | `AmityCommunityEvents.COMMUNITY` | `AmityCommunityEvents.COMMUNITY` | | Posts in the community | `SubscriptionLevels.POST` | `.posts` | `AmityCommunityEvents.POSTS` | `AmityCommunityEvents.POSTS` | | Comments in the community | `SubscriptionLevels.COMMENT` | `.comments` | `AmityCommunityEvents.COMMENTS` | `AmityCommunityEvents.COMMENTS` | | Posts and comments | `SubscriptionLevels.POST_AND_COMMENT` | `.postsAndComments` | `AmityCommunityEvents.POSTS_AND_COMMENTS` | `AmityCommunityEvents.POSTS_AND_COMMENTS` | | Stories and story comments | Use `getCommunityStoriesTopic(...)` | `.storyAndComments` | `AmityCommunityEvents.STORIES_AND_COMMENTS` | `AmityCommunityEvents.STORIES_AND_COMMENTS` | ### Parameters | Platform | Parameter | Required | Description | | --- | --- | --- | --- | | TypeScript | `community` | Yes | `Amity.Community` or any subscribable object with a valid `path`. | | TypeScript | `level` | No | Defaults to `SubscriptionLevels.COMMUNITY`. Use `POST`, `COMMENT`, or `POST_AND_COMMENT` for content updates. | | iOS | `community` | Yes | `AmityCommunity` model used to construct `AmityCommunityTopic`. | | iOS | `andEvent` | Yes | One `AmityCommunityEvent` value. | | Android | `events` | Yes | One `AmityCommunityEvents` value passed to `community.subscription(events)`. | | Flutter | `events` | Yes | One `AmityCommunityEvents` value passed to `community.subscription(events)`. | ```typescript TypeScript import { getCommunityTopic, subscribeTopic, SubscriptionLevels } from '@amityco/ts-sdk'; function subscribeToCommunityFeed(community: Amity.Community): Amity.Unsubscriber { const topic = getCommunityTopic( community, SubscriptionLevels.POST_AND_COMMENT, ); return subscribeTopic(topic); } ``` ```swift iOS func subscribeToCommunityFeed(_ community: AmityCommunity) async throws { let topic = AmityCommunityTopic( community: community, andEvent: .postsAndComments ) try await AmityTopicSubscription().subscribeTopic(topic) } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.core.events.AmityCommunityEvents import com.amity.socialcloud.sdk.model.social.community.AmityCommunity fun subscribeToCommunityFeed(community: AmityCommunity) { community.subscription(AmityCommunityEvents.POSTS_AND_COMMENTS) .subscribeTopic() .subscribe({ showSuccessMessage("Subscribed") }, { error -> showErrorMessage(error = error) }) } ``` ```dart Flutter Future subscribeToCommunityFeed(AmityCommunity community) async { await community .subscription(AmityCommunityEvents.POSTS_AND_COMMENTS) .subscribeTopic(); } ``` ## Post Topic Use a post topic for a single post detail screen or a moderation/action surface focused on one post. ### Event Levels | Intent | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Post object changes | `getPostTopic(post)` or `SubscriptionLevels.POST` | `.post` | `AmityPostEvents.POST` | `AmityPostEvents.POST` | | Comments on the post | `SubscriptionLevels.COMMENT` | `.comments` | `AmityPostEvents.COMMENTS` | `AmityPostEvents.COMMENTS` | | Live reactions | `getLiveReactionTopic(post)` | `.liveReaction` | `AmityPostEvents.LIVE_REACTION` | Not exposed in this checkout | ### Parameters | Platform | Parameter | Required | Description | | --- | --- | --- | --- | | TypeScript | `post` | Yes | `Amity.Post` or subscribable post object with a valid `path`. | | TypeScript | `level` | No | Defaults to post object events. Use `SubscriptionLevels.COMMENT` for post comments. | | iOS | `post` | Yes | `AmityPost` model used to construct `AmityPostTopic`. | | iOS | `andEvent` | Yes | One `AmityPostEvent` value. | | Android | `events` | Yes | One `AmityPostEvents` value passed to `post.subscription(events)`. | | Flutter | `events` | Yes | One `AmityPostEvents` value passed to `post.subscription(events)`. | ```typescript TypeScript import { getPostTopic, subscribeTopic, SubscriptionLevels } from '@amityco/ts-sdk'; function subscribeToPostComments(post: Amity.Post): Amity.Unsubscriber { const topic = getPostTopic(post, SubscriptionLevels.COMMENT); return subscribeTopic(topic); } ``` ```swift iOS func subscribeToPostComments(_ post: AmityPost) async throws { let topic = AmityPostTopic(post: post, andEvent: .comments) try await AmityTopicSubscription().subscribeTopic(topic) } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.core.events.AmityPostEvents import com.amity.socialcloud.sdk.model.social.post.AmityPost fun subscribeToPostComments(post: AmityPost) { post.subscription(AmityPostEvents.COMMENTS) .subscribeTopic() .subscribe({ showSuccessMessage("Subscribed") }, { error -> showErrorMessage(error = error) }) } ``` ```dart Flutter Future subscribeToPostComments(AmityPost post) async { await post .subscription(AmityPostEvents.COMMENTS) .subscribeTopic(); } ``` ## Comment Topic Use a comment topic for a focused comment detail, moderation, or realtime reply surface. ### Event Levels | Intent | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Comment object changes | `getCommentTopic(comment)` | `.comment` | `AmityCommentEvents.COMMENT` | `AmityCommentEvents.COMMENT` | ### Parameters | Platform | Parameter | Required | Description | | --- | --- | --- | --- | | TypeScript | `comment` | Yes | `Amity.Comment` or subscribable comment object with a valid `path`. | | iOS | `comment` | Yes | `AmityComment` model used to construct `AmityCommentTopic`. | | iOS | `andEvent` | Yes | `.comment`. | | Android | `events` | Yes | `AmityCommentEvents.COMMENT`. | | Flutter | `events` | Yes | `AmityCommentEvents.COMMENT`. | ```typescript TypeScript import { getCommentTopic, subscribeTopic } from '@amityco/ts-sdk'; function subscribeToComment(comment: Amity.Comment): Amity.Unsubscriber { const topic = getCommentTopic(comment); return subscribeTopic(topic); } ``` ```swift iOS func subscribeToComment(_ comment: AmityComment) async throws { let topic = AmityCommentTopic(comment: comment, andEvent: .comment) try await AmityTopicSubscription().subscribeTopic(topic) } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.core.events.AmityCommentEvents import com.amity.socialcloud.sdk.model.social.comment.AmityComment fun subscribeToComment(comment: AmityComment) { comment.subscription(AmityCommentEvents.COMMENT) .subscribeTopic() .subscribe({ showSuccessMessage("Subscribed") }, { error -> showErrorMessage(error = error) }) } ``` ```dart Flutter Future subscribeToComment(AmityComment comment) async { await comment .subscription(AmityCommentEvents.COMMENT) .subscribeTopic(); } ``` ## User Topic Use a user topic when a screen needs live updates for a user profile or that user's feed activity. ### Event Levels | Intent | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | User object changes | `SubscriptionLevels.USER` or omit the level | `.user` | `AmityUserEvents.USER` | `AmityUserEvents.USER` | | User posts | `SubscriptionLevels.POST` | `.posts` | `AmityUserEvents.POSTS` | `AmityUserEvents.POSTS` | | User comments | `SubscriptionLevels.COMMENT` | `.comments` | `AmityUserEvents.COMMENTS` | `AmityUserEvents.COMMENTS` | | User posts and comments | `SubscriptionLevels.POST_AND_COMMENT` | `.postsAndComments` | `AmityUserEvents.POSTS_AND_COMMENTS` | `AmityUserEvents.POSTS_AND_COMMENTS` | ### Parameters | Platform | Parameter | Required | Description | | --- | --- | --- | --- | | TypeScript | `user` | Yes | `Amity.User` or subscribable user object with a valid `path`. | | TypeScript | `level` | No | Defaults to `SubscriptionLevels.USER`. Use content levels for user-feed activity. | | iOS | `user` | Yes | `AmityUser` model used to construct `AmityUserTopic`. | | iOS | `andEvent` | Yes | One `AmityUserEvent` value. | | Android | `events` | Yes | One `AmityUserEvents` value passed to `user.subscription(events)`. | | Flutter | `events` | Yes | One `AmityUserEvents` value passed to `user.subscription(events)`. | ```typescript TypeScript import { getUserTopic, subscribeTopic, SubscriptionLevels } from '@amityco/ts-sdk'; function subscribeToUserPosts(user: Amity.User): Amity.Unsubscriber { const topic = getUserTopic(user, SubscriptionLevels.POST); return subscribeTopic(topic); } ``` ```swift iOS func subscribeToUserPosts(_ user: AmityUser) async throws { let topic = AmityUserTopic(user: user, andEvent: .posts) try await AmityTopicSubscription().subscribeTopic(topic) } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.core.events.AmityUserEvents import com.amity.socialcloud.sdk.model.core.user.AmityUser fun subscribeToUserPosts(user: AmityUser) { user.subscription(AmityUserEvents.POSTS) .subscribeTopic() .subscribe({ showSuccessMessage("Subscribed") }, { error -> showErrorMessage(error = error) }) } ``` ```dart Flutter Future subscribeToUserPosts(AmityUser user) async { await user .subscription(AmityUserEvents.POSTS) .subscribeTopic(); } ``` ## Follow Topic Follow topics observe changes to the current user's follower and following lists. The current Flutter SDK checkout does not expose a public follow topic subscription helper. Use this section for TypeScript, iOS, and Android. ### Event Levels | Intent | TypeScript | iOS | Android | | --- | --- | --- | --- | | Current user's followers | `getMyFollowersTopic()` | `.myFollowers` | `AmityFollowEvents.MY_FOLLOWERS` | | Current user's following | `getMyFollowingsTopic()` | `.myFollowing` | `AmityFollowEvents.MY_FOLLOWINGS` | ### Parameters | Platform | Parameter | Required | Description | | --- | --- | --- | --- | | TypeScript | None | No | Helpers derive the current user from the active SDK user. | | iOS | `event` | Yes | One `AmityFollowEvent` value. | | Android | `events` | Yes | One `AmityFollowEvents` value used to construct `AmityTopic.FOLLOW(...)`. | ```typescript TypeScript import { getMyFollowersTopic, subscribeTopic } from '@amityco/ts-sdk'; function subscribeToMyFollowers(): Amity.Unsubscriber { const topic = getMyFollowersTopic(); return subscribeTopic(topic); } ``` ```swift iOS func subscribeToMyFollowers() async throws { let topic = AmityFollowTopic(event: .myFollowers) try await AmityTopicSubscription().subscribeTopic(topic) } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.core.AmityCoreClient import com.amity.socialcloud.sdk.model.core.events.AmityFollowEvents import com.amity.socialcloud.sdk.model.core.events.AmityTopic fun subscribeToMyFollowers() { AmityCoreClient.subscription(AmityTopic.FOLLOW(AmityFollowEvents.MY_FOLLOWERS)) .subscribeTopic() .subscribe({ showSuccessMessage("Subscribed") }, { error -> showErrorMessage(error = error) }) } ``` ## Story Topic Story topics observe one story or a community's story stream. ### Event Levels | Intent | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | One story | `getStoryTopic(story)` | `AmityStoryTopic(story:andEvent:)` with `.story` | `story.subscription()` | `story.subscription()` | | Community story stream | `getCommunityStoriesTopic(...)` | Community topic `.storyAndComments` | Community topic `STORIES_AND_COMMENTS` | Community topic `STORIES_AND_COMMENTS` | ### Parameters | Platform | Parameter | Required | Description | | --- | --- | --- | --- | | TypeScript | `story` | Yes | Object with `targetId`, `targetType`, and `storyId` for `getStoryTopic`. | | TypeScript | `targetId`, `targetType` | Yes | Community or user story target passed to `getCommunityStoriesTopic`. | | iOS | `story` | Yes | `AmityStory` model used to construct `AmityStoryTopic`. | | iOS | `andEvent` | Yes | `.story`. | | Android | `story` | Yes | `AmityStory` model exposing `subscription()`. | | Flutter | `story` | Yes | `AmityStory` model exposing `subscription()`. | ```typescript TypeScript import { getStoryTopic, subscribeTopic } from '@amityco/ts-sdk'; function subscribeToStory( story: Pick, ): Amity.Unsubscriber { const topic = getStoryTopic(story); return subscribeTopic(topic); } ``` ```swift iOS func subscribeToStory(_ story: AmityStory) async throws { let topic = AmityStoryTopic(story: story, andEvent: .story) try await AmityTopicSubscription().subscribeTopic(topic) } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.social.story.AmityStory import com.amity.socialcloud.sdk.model.social.story.subscription fun subscribeToStory(story: AmityStory) { story.subscription() .subscribeTopic() .subscribe({ showSuccessMessage("Subscribed") }, { error -> showErrorMessage(error = error) }) } ``` ```dart Flutter Future subscribeToStory(AmityStory story) async { await story.subscription().subscribeTopic(); } ``` ## Cleanup Use the platform-specific unsubscribe path when the UI no longer needs the topic. ```typescript TypeScript import { getCommunityTopic, subscribeTopic, SubscriptionLevels } from '@amityco/ts-sdk'; function mountCommunityFeed(community: Amity.Community): Amity.Unsubscriber { const topic = getCommunityTopic( community, SubscriptionLevels.POST_AND_COMMENT, ); return subscribeTopic(topic); } const unsubscribe = mountCommunityFeed(community); unsubscribe(); ``` ```swift iOS func unsubscribeFromCommunityFeed(_ community: AmityCommunity) async throws { let topic = AmityCommunityTopic( community: community, andEvent: .postsAndComments ) try await AmityTopicSubscription().unsubscribeTopic(topic) } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.core.events.AmityCommunityEvents import com.amity.socialcloud.sdk.model.social.community.AmityCommunity fun unsubscribeFromCommunityFeed(community: AmityCommunity) { community.subscription(AmityCommunityEvents.POSTS_AND_COMMENTS) .unsubscribeTopic() .subscribe() } ``` ```dart Flutter Future unsubscribeFromCommunityFeed(AmityCommunity community) async { await community .subscription(AmityCommunityEvents.POSTS_AND_COMMENTS) .unsubscribeTopic(); } ``` ## Related Topics Learn how topic subscriptions fit with live objects and collections. Subscribe to subchannel topics for chat threads. --- ### [Presence State](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/presence-state/overview) > Understand which social.plus SDKs support user, channel, and room presence, and choose the right presence API. Presence state lets your app show who is currently active. The SDK has two different presence surfaces: - **Network presence** tracks the current user's heartbeat and lets iOS and Android apps read or sync other users' online state. - **Room presence** tracks who is currently watching a live room. It is available on iOS, Android, and TypeScript. Presence APIs require the presence feature to be enabled for your network. If the feature is disabled, heartbeat calls can fail with an SDK error. ## Platform Surface | Capability | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Current user heartbeat | Not available | `client.presence` | `AmityCoreClient.presence()` | Not available | | User presence | Not available | `AmityUserPresenceRepository` | `AmityCoreClient.newUserPresenceRepository()` | Not available | | Channel presence | Not available | `AmityChannelPresenceRepository` | `AmityChatClient.newChannelPresenceRepository()` | Not available | | Room presence | `RoomPresenceRepository` | `AmityRoomPresenceRepository` | `AmityCoreClient.newRoomPresenceRepository()` | Not available | ## Which Presence API to Use Read specific users' online state, sync visible users in a list, or fetch a snapshot of online users. Show whether any other member of a conversation channel is online. Count and list users currently watching a live room. Start and stop the heartbeat that marks the current user or room viewer as active. ## Behavior to Know iOS and Android expose `isOnline` as a computed value from the user's last heartbeat. The SDK considers a user online while the last heartbeat is recent. User and channel sync APIs are meant for visible lists. Start syncing when an item appears, then unsync when it disappears so you stay within the SDK sync limits. Room presence uses room-specific heartbeat and count APIs. Use it for live rooms and viewer lists, not for general "online user" badges. The Flutter SDK in this checkout does not expose user, channel, or room presence repositories. Do not copy iOS, Android, or TypeScript presence snippets into Flutter integrations. ## Related Topics Use live data objects for SDK resources that publish real-time updates. Learn how event subscriptions power live chat and social experiences. --- ### [User Presence](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/presence-state/user-presence) > Read and sync user online status with AmityUserPresenceRepository on iOS and Android. User presence lets iOS and Android apps read whether specific users are currently online. Use it for online badges, visible member lists, or a lightweight "who is online" snapshot. The current TypeScript and Flutter SDKs in this checkout do not expose a user presence repository. Use the iOS and Android snippets below only for those platforms. ## Platform Surface | Platform | Repository | Main APIs | | --- | --- | --- | | iOS | `AmityUserPresenceRepository()` | `getUserPresence(userIds:)`, `syncUserPresence(id:viewId:)`, `getSyncingUserPresence()`, `getOnlineUsersCount()`, `getOnlineUsersSnapshot()` | | Android | `AmityCoreClient.newUserPresenceRepository()` | `getUserPresence(userIds)`, `syncUserPresence(userId, viewId)`, `getSyncingUserPresence()`, `getOnlineUsersCount()`, `getOnlineUsersSnapshot()` | | TypeScript | Not available | Not available | | Flutter | Not available | Not available | ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | `getUserPresence` | `userIds` | Yes | User IDs to read. iOS documents a maximum of 220 IDs per call. Keep Android calls bounded for the same reason. | | `syncUserPresence` | `id` / `userId` | Yes | User ID whose presence should be refreshed periodically while visible. | | `syncUserPresence` | `viewId` | No | Stable view identifier. Defaults to `amity-global`; use a custom value when the same user can appear in multiple visible UI locations. | | `unsyncUserPresence` | `id` / `userId` | Yes | User ID to remove from syncing. | | `unsyncUserPresence` | `viewId` | No | Must match the `viewId` used when syncing if you passed a custom value. | | `unsyncAllUserPresence` | None | No | Stops syncing all user presence tracked by the SDK presence engine. | | `getSyncingUserPresence` | None | No | Returns iOS `AnyPublisher<[AmityUserPresence], Error>` or Android `Flowable>`. | | `getOnlineUsersCount` | None | No | Returns the current count of online users in the network. | | `getOnlineUsersSnapshot` | None | No | Returns a point-in-time, paginated snapshot of online users. iOS and Android load users 20 at a time. | ## User Presence Object | Field | iOS | Android | Description | | --- | --- | --- | --- | | User ID | `presence.userId` | `presence.getUserId()` | The user represented by this presence record. | | Last heartbeat | `presence.lastHeartbeat` | `presence.getLastHeartbeat()` | Last heartbeat timestamp, when available. | | Online state | `presence.isOnline` | `presence.isOnline()` | `true` when the last heartbeat is within the SDK online threshold. | The SDK limits active presence syncing to 20 user IDs at a time. Unsync users when their row, card, or profile preview is no longer visible. ## Read User Presence Fetch presence for known users as a one-shot read. ```swift iOS let repository = AmityUserPresenceRepository() let presences = try await repository.getUserPresence(userIds: [userId]) for presence in presences { let isOnline = presence.isOnline let lastHeartbeat = presence.lastHeartbeat showSuccessMessage("\(presence.userId): \(isOnline)") } ``` ```kotlin Android val repository = AmityCoreClient.newUserPresenceRepository() repository.getUserPresence(listOf(userId)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ presences -> presences.forEach { presence -> val isOnline = presence.isOnline() val lastHeartbeat = presence.getLastHeartbeat() showSuccessMessage("${presence.getUserId()}: $isOnline") } }, { error -> showErrorMessage(error = error) }) ``` ## Sync User Presence Sync user presence while users are visible, then unsync them when the UI no longer needs updates. ```swift iOS let repository = AmityUserPresenceRepository() let cancellable = repository.getSyncingUserPresence() .sink(receiveCompletion: { completion in if case let .failure(error) = completion { handleError(error) } }, receiveValue: { presences in for presence in presences { showSuccessMessage("\(presence.userId): \(presence.isOnline)") } }) repository.syncUserPresence(id: userId) // Call when the row or screen is no longer visible. repository.unsyncUserPresence(id: userId) _ = cancellable ``` ```kotlin Android val repository = AmityCoreClient.newUserPresenceRepository() repository.getSyncingUserPresence() .observeOn(AndroidSchedulers.mainThread()) .subscribe({ presences -> presences.forEach { presence -> showSuccessMessage("${presence.getUserId()}: ${presence.isOnline()}") } }, { error -> showErrorMessage(error = error) }) repository.syncUserPresence(userId) // Call when the row or screen is no longer visible. repository.unsyncUserPresence(userId) ``` ## Read Online Users Count And Snapshot Use the count for a network-wide online total. Use the snapshot when you need actual user objects at the time of the query. ```swift iOS let repository = AmityUserPresenceRepository() let count = try await repository.getOnlineUsersCount() showSuccessMessage(count) let snapshot = try await repository.getOnlineUsersSnapshot() let users = snapshot.users if snapshot.canLoadMore { await snapshot.loadMore() } ``` ```kotlin Android val repository = AmityCoreClient.newUserPresenceRepository() repository.getOnlineUsersCount() .subscribe({ count -> showSuccessMessage(count) }, { error -> showErrorMessage(error = error) }) repository.getOnlineUsersSnapshot() .subscribe({ snapshot -> val users = snapshot.getUsers() if (snapshot.canLoadMore()) { snapshot.loadMore().subscribe() } }, { error -> showErrorMessage(error = error) }) ``` ## Best Practices Other users can only see the current user as online if the app enables presence and starts the current user's heartbeat. See Heartbeat Sync. Bind `syncUserPresence` to visible UI rows, cells, or cards. Call `unsyncUserPresence` during cell reuse, unmount, or screen dismissal. `getOnlineUsersSnapshot` is a point-in-time read. For live online badges, sync specific visible users instead. ## Related Topics Show whether any other member of a conversation channel is online. Mark the current user as online with the SDK heartbeat. --- ### [Channel Presence](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/presence-state/channel-presence) > Sync conversation channel member presence with AmityChannelPresenceRepository on iOS and Android. Channel presence tells your app whether any other member of a conversation channel is online. Use it for chat lists, member lists, and compact "active now" indicators. Channel presence is available on iOS and Android only. The current TypeScript and Flutter SDKs in this checkout do not expose channel presence APIs. ## Platform Surface | Platform | Repository | Main APIs | | --- | --- | --- | | iOS | `AmityChannelPresenceRepository()` | `syncChannelPresence(id:viewId:)`, `unsyncChannelPresence(id:viewId:)`, `unsyncAllChannelPresence()`, `getSyncingChannelPresence()` | | Android | `AmityChatClient.newChannelPresenceRepository()` | `syncChannelPresence(channelId, viewId)`, `unsyncChannelPresence(channelId, viewId)`, `unsyncAllChannelPresence()`, `getSyncingChannelPresence()` | | TypeScript | Not available | Not available | | Flutter | Not available | Not available | Only conversation channels support channel presence. The SDK sync limit is 20 channel IDs at a time. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | `syncChannelPresence` | `id` / `channelId` | Yes | Conversation channel ID whose members' presence should be refreshed periodically. | | `syncChannelPresence` | `viewId` | No | Stable view identifier. Defaults to `amity-global`; use a custom value when the same channel can appear in multiple visible UI locations. | | `unsyncChannelPresence` | `id` / `channelId` | Yes | Channel ID to remove from syncing. | | `unsyncChannelPresence` | `viewId` | No | Must match the `viewId` used when syncing if you passed a custom value. | | `unsyncAllChannelPresence` | None | No | Stops syncing all channel presence tracked by the SDK presence engine. | | `getSyncingChannelPresence` | None | No | Returns iOS `AnyPublisher<[AmityChannelPresence], Error>` or Android `Flowable>`. | ## Channel Presence Object | Field | iOS | Android | Description | | --- | --- | --- | --- | | Channel ID | `presence.channelId` | `presence.getChannelId()` | The conversation channel represented by the presence record. | | User presences | `presence.userPresences` | `presence.getUserPresences()` | Presence records for members in the channel. | | Any other member online | `presence.isAnyMemberOnline` | `presence.isAnyMemberOnline()` | `true` when at least one member other than the current user is online. | ## Sync Channel Presence Start syncing when a conversation channel becomes visible, then unsync it when the row or screen is gone. ```swift iOS let repository = AmityChannelPresenceRepository() let cancellable = repository.getSyncingChannelPresence() .sink(receiveCompletion: { completion in if case let .failure(error) = completion { handleError(error) } }, receiveValue: { presences in for presence in presences { let isAnyMemberOnline = presence.isAnyMemberOnline showSuccessMessage("\(presence.channelId): \(isAnyMemberOnline)") } }) repository.syncChannelPresence(id: channelId) // Call when the row or screen is no longer visible. repository.unsyncChannelPresence(id: channelId) _ = cancellable ``` ```kotlin Android val repository = AmityChatClient.newChannelPresenceRepository() repository.getSyncingChannelPresence() .observeOn(AndroidSchedulers.mainThread()) .subscribe({ presences -> presences.forEach { presence -> val isAnyMemberOnline = presence.isAnyMemberOnline() showSuccessMessage("${presence.getChannelId()}: $isAnyMemberOnline") } }, { error -> showErrorMessage(error = error) }) repository.syncChannelPresence(channelId) // Call when the row or screen is no longer visible. repository.unsyncChannelPresence(channelId) ``` ## Unsync All Channel Presence Use `unsyncAllChannelPresence` when leaving a channel list, tearing down a view model, or replacing the whole visible channel set. ```swift iOS let repository = AmityChannelPresenceRepository() repository.syncChannelPresence(id: channelId) // Later, when the channel list is dismissed. repository.unsyncAllChannelPresence() ``` ```kotlin Android val repository = AmityChatClient.newChannelPresenceRepository() repository.syncChannelPresence(channelId) // Later, when the channel list is dismissed. repository.unsyncAllChannelPresence() ``` ## Best Practices Sync a channel when its conversation row appears. Unsync during row reuse, unmount, or screen dismissal so the app stays under the 20-channel sync limit. `isAnyMemberOnline` is designed for compact channel-list indicators. If you need per-user badges, sync those users directly with User Presence. Other members can only see the current user as online if the app enables presence and starts the current user's heartbeat. ## Related Topics Sync specific users when you need per-user online badges. Mark the current user as online with the SDK heartbeat. --- ### [Heartbeat Sync](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/presence-state/heartbeat-sync) > Start and stop SDK presence heartbeats for the current user or a live room viewer. A heartbeat tells social.plus that the current user is still active. There are two heartbeat surfaces: - **Current user heartbeat** marks the logged-in user online for user and channel presence. It is available on iOS and Android. - **Room heartbeat** marks the current user as present in a live room. It is available on iOS, Android, and TypeScript. Heartbeat intervals are controlled by server-side presence settings. The SDK sends the first heartbeat immediately, then continues on the configured cadence. ## Platform Surface | Heartbeat | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Current user heartbeat | Not available | `client.presence.startHeartbeat()` | `AmityCoreClient.presence().startHeartbeat()` | Not available | | Room heartbeat | `RoomPresenceRepository.startHeartbeat(roomId)` | `AmityRoomPresenceRepository(roomId:)` | `AmityCoreClient.newRoomPresenceRepository().roomId(roomId)` | Not available | ## Parameters | Operation | Platform | Parameter | Required | Description | | --- | --- | --- | --- | --- | | `presence.enable` / `presence().enable` | iOS, Android | None | No | Enables user presence for the current user. | | `presence.isEnabled` / `presence().isEnabled` | iOS, Android | None | No | Checks whether user presence is enabled for the current user. | | `presence.startHeartbeat` / `presence().startHeartbeat` | iOS, Android | None | No | Starts the current user's network presence heartbeat. | | `presence.stopHeartbeat` / `presence().stopHeartbeat` | iOS, Android | None | No | Stops the current user's network presence heartbeat. | | `RoomPresenceRepository.startHeartbeat` | TypeScript | `roomId` | Yes | Room ID where the current user should be counted as present. | | `AmityRoomPresenceRepository(roomId:)` | iOS | `roomId` | Yes | Room ID used to create the room presence repository. | | `newRoomPresenceRepository().roomId(roomId)` | Android | `roomId` | Yes | Room ID used to create the room presence repository. | ## Start the Current User Heartbeat Use this when your app wants the current user to appear online in user and channel presence. ```swift iOS let isEnabled = try await client.presence.isEnabled() if !isEnabled { try await client.presence.enable() } try await client.presence.startHeartbeat() // Call when the user should no longer appear online. client.presence.stopHeartbeat() ``` ```kotlin Android AmityCoreClient.presence().isEnabled() .flatMapCompletable { isEnabled -> val enableIfNeeded = if (isEnabled) { Completable.complete() } else { AmityCoreClient.presence().enable() } enableIfNeeded.andThen(AmityCoreClient.presence().startHeartbeat()) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ showSuccessMessage("Presence heartbeat started") }, { error -> showErrorMessage(error = error) }) // Call when the user should no longer appear online. AmityCoreClient.presence().stopHeartbeat() ``` ## Start a Room Heartbeat Use room heartbeat only while the current user is watching or participating in a live room. ```swift iOS let roomPresence = AmityRoomPresenceRepository(roomId: roomId) try await roomPresence.startHeartbeat() // Call when the viewer leaves the room. roomPresence.stopHeartbeat() ``` ```kotlin Android val roomPresence = AmityCoreClient.newRoomPresenceRepository().roomId(roomId) roomPresence.startHeartbeat() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ showSuccessMessage("Room heartbeat started") }, { error -> showErrorMessage(error = error) }) // Call when the viewer leaves the room. roomPresence.stopHeartbeat() ``` ```typescript TypeScript import { RoomPresenceRepository } from '@amityco/ts-sdk'; RoomPresenceRepository.startHeartbeat(roomId); // Call when the viewer leaves the room. RoomPresenceRepository.stopHeartbeat(roomId); ``` ## Lifecycle Guidance Start the current user heartbeat after login when the app wants the user to appear online. Start room heartbeat only while the room screen is visible and the user should count as a viewer. Stop heartbeat when the user logs out, leaves the room, dismisses the screen, or your view model is deallocated. For mobile apps, also stop or pause room heartbeat when the room screen backgrounds. If presence is disabled for the network, heartbeat start calls can fail. Treat that as a configuration error and hide online-state UI instead of retrying forever. ## Related Topics Read and sync user online state after the current user heartbeat is active. Read live room viewer counts and online room users. --- ### [Room Presence](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/presence-state/room-presence) > Track live room viewer heartbeat, viewer count, and online room users with room presence APIs. Room presence tracks who is currently watching a live room. Use it for viewer counts, "who is watching" panels, and host tools that need to know which users are present in a room right now. Room presence is available on iOS, Android, and TypeScript. It is not available in the current Flutter SDK checkout. ## Platform Surface | Platform | Repository | Main APIs | | --- | --- | --- | | TypeScript | `RoomPresenceRepository` | `startHeartbeat(roomId)`, `stopHeartbeat(roomId)`, `getRoomUserCount(roomId)`, `getRoomOnlineUsers(roomId)` | | iOS | `AmityRoomPresenceRepository(roomId:)` | `startHeartbeat()`, `stopHeartbeat()`, `getRoomUserCount()`, `getRoomOnlineUsers()` | | Android | `AmityCoreClient.newRoomPresenceRepository().roomId(roomId)` | `startHeartbeat()`, `stopHeartbeat()`, `getOnlineUsersCount()`, `observeOnlineUsersCount(interval:)`, `getOnlineUsersSnapshot()` | | Flutter | Not available | Not available | ## Parameters | Operation | Platform | Parameter | Required | Description | | --- | --- | --- | --- | --- | | Repository creation | iOS, Android | `roomId` | Yes | Room ID whose presence should be tracked. | | `startHeartbeat` | TypeScript | `roomId` | Yes | Room ID where the current user should be counted as present. | | `startHeartbeat` | iOS, Android | None | No | Uses the `roomId` already bound to the repository. | | `stopHeartbeat` | TypeScript | `roomId` | Yes | Room ID whose heartbeat should stop. | | `stopHeartbeat` | iOS, Android | None | No | Stops the active room heartbeat managed by the repository or presence service. | | `getRoomUserCount` / `getOnlineUsersCount` | All supported platforms | None or `roomId` | Platform-dependent | Reads the current online viewer count for the room. | | `observeOnlineUsersCount` | Android | `interval` | No | Poll interval in seconds. Defaults to 15 seconds. | | `getRoomOnlineUsers` / `getOnlineUsersSnapshot` | All supported platforms | None or `roomId` | Platform-dependent | Reads a point-in-time list or snapshot of online room users. Android snapshots paginate 20 users at a time. | ## Start Room Heartbeat Start the room heartbeat when the user enters the room, and stop it when they leave. The SDK sends the first heartbeat immediately and continues at the server-configured interval. ```swift iOS let roomPresence = AmityRoomPresenceRepository(roomId: roomId) try await roomPresence.startHeartbeat() // Call when the viewer leaves the room. roomPresence.stopHeartbeat() ``` ```kotlin Android val roomPresence = AmityCoreClient.newRoomPresenceRepository().roomId(roomId) roomPresence.startHeartbeat() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ showSuccessMessage("Room heartbeat started") }, { error -> showErrorMessage(error = error) }) // Call when the viewer leaves the room. roomPresence.stopHeartbeat() ``` ```typescript TypeScript import { RoomPresenceRepository } from '@amityco/ts-sdk'; RoomPresenceRepository.startHeartbeat(roomId); // Call when the viewer leaves the room. RoomPresenceRepository.stopHeartbeat(roomId); ``` ## Read Room User Count Use the count as the source of truth for how many users are currently present in the room. ```swift iOS let roomPresence = AmityRoomPresenceRepository(roomId: roomId) let count = try await roomPresence.getRoomUserCount() showSuccessMessage(count) ``` ```kotlin Android val roomPresence = AmityCoreClient.newRoomPresenceRepository().roomId(roomId) roomPresence.getOnlineUsersCount() .subscribe({ count -> showSuccessMessage(count) }, { error -> showErrorMessage(error = error) }) ``` ```typescript TypeScript import { RoomPresenceRepository } from '@amityco/ts-sdk'; const { count } = await RoomPresenceRepository.getRoomUserCount(roomId); updateUI(count); ``` ## Keep the Count Live The count is a point-in-time read. Android provides a built-in polling stream. On iOS and TypeScript, use your own timer and choose an interval that fits the UI. ```swift iOS let roomPresence = AmityRoomPresenceRepository(roomId: roomId) let timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { _ in Task { @MainActor in if let count = try? await roomPresence.getRoomUserCount() { showSuccessMessage(count) } } } timer.invalidate() ``` ```kotlin Android val roomPresence = AmityCoreClient.newRoomPresenceRepository().roomId(roomId) roomPresence.observeOnlineUsersCount(interval = 30) .subscribe({ count -> showSuccessMessage(count) }, { error -> showErrorMessage(error = error) }) ``` ```typescript TypeScript import { RoomPresenceRepository } from '@amityco/ts-sdk'; const timer = setInterval(async () => { const { count } = await RoomPresenceRepository.getRoomUserCount(roomId); updateUI(count); }, 30_000); clearInterval(timer); ``` ## Read Room Online Users Use the online-user list when the app needs actual user objects, such as avatars in a "who is watching" panel. For large rooms, display the count as the total and treat the list as a snapshot. ```swift iOS let roomPresence = AmityRoomPresenceRepository(roomId: roomId) let users = try await roomPresence.getRoomOnlineUsers() displayUserList(users) ``` ```kotlin Android val roomPresence = AmityCoreClient.newRoomPresenceRepository().roomId(roomId) roomPresence.getOnlineUsersSnapshot() .subscribe({ snapshot -> val users = snapshot.getUsers() if (snapshot.canLoadMore()) { snapshot.loadMore().subscribe() } }, { error -> showErrorMessage(error = error) }) ``` ```typescript TypeScript import { RoomPresenceRepository } from '@amityco/ts-sdk'; const { data: users } = await RoomPresenceRepository.getRoomOnlineUsers(roomId); renderResults(users); ``` ## Best Practices Start room heartbeat when the viewer enters the live room screen. Stop it when the viewer leaves, switches rooms, backgrounds the screen, or the view model is torn down. The count is the total active viewer number. The online-user list is best for UI that needs actual user objects, such as avatars or invite-to-cohost flows. Android defaults `observeOnlineUsersCount` to 15 seconds. On iOS and TypeScript, avoid very aggressive polling unless the count is central to the experience. ## Related Topics Learn when to start and stop current user and room heartbeats. Create social posts that reference live rooms. --- ### [Logging & Errors](https://learn.social.plus/social-plus-sdk/core-concepts/foundation/logging) > Monitor SDK network activity during integration and handle SDK error codes safely. Use SDK logging and error surfaces while you build and debug an integration. Keep request and response logging out of production logs unless your app has an explicit redaction and retention policy. ## SDK Surface | Need | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Network activity observer | `Client.onNetworkActivities(callback)` | `client.observeNetworkActivities(_:)` | `AmityCoreClient.observeNetworkActivities()` | Not exposed as a public SDK observer | | Error parsing | `error.code` from SDK errors and connection events | `AmityErrorCode(rawValue:)` | `AmityError.from(...)` | `AmityException.toAmityError()` | | Global ban signal | `Amity.ServerError.GLOBAL_BAN` | `.globalBan` | `AmityError.USER_IS_GLOBAL_BANNED` | `AmityError.USER_IS_GLOBAL_BANNED` | Use the network observer as a development diagnostic tool. It can expose request metadata, response metadata, and payloads depending on platform, so avoid storing raw logs that may contain user content or credentials. ## Parameters | Platform | Callback or stream value | Notes | | --- | --- | --- | | TypeScript | `request`, `response` | `response` includes `data`, `status`, `statusText`, and `headers`. The API returns an unsubscriber. | | iOS | `URLRequest?`, `HTTPURLResponse?`, `Data?` | Pass `nil` to `observeNetworkActivities` when logging should stop. | | Android | `Flowable` | Subscribe while debugging and dispose the subscription when finished. Avoid depending on internal activity subtypes in app code. | | Flutter | Not available | Use normal request-level error handling and your app's HTTP tooling for Flutter-specific network diagnostics. | ## Network Activity Observe network activity only while debugging an integration, then unsubscribe or dispose the observer when the debug session ends. ```typescript TypeScript import { Client } from '@amityco/ts-sdk'; const unsubscribe = Client.onNetworkActivities((request, response) => { console.log(request.method, request.url); console.log(response.status, response.statusText); console.log(response.data); }); // Stop observing when the debug session ends. unsubscribe(); ``` ```swift iOS client.observeNetworkActivities { request, response, data in if let request { print("\(request.httpMethod ?? "GET") \(request.url?.absoluteString ?? "")") } if let response { print("status: \(response.statusCode)") } if let data { print("response bytes: \(data.count)") } } // Stop observing when the debug session ends. client.observeNetworkActivities(nil) ``` ```kotlin Android val disposable = AmityCoreClient.observeNetworkActivities() .doOnNext { activity -> print(activity) } .doOnError { exception -> val amityError = AmityError.from(exception) print(amityError) } .subscribe() // Stop observing when the debug session ends. disposable.dispose() ``` ## Error Handling SDK operations throw or emit platform-native errors. Convert those errors to SDK error enums before deciding whether to retry, show a user message, or end the session. ```typescript TypeScript import { Client } from '@amityco/ts-sdk'; const unsubscribe = Client.onConnectionError(error => { if (error.code === Amity.ServerError.GLOBAL_BAN) { console.log('The current user is globally banned.'); } }); unsubscribe(); ``` ```swift iOS func handleSdkError(_ error: Error) { let sdkError = error as NSError guard let amityError = AmityErrorCode(rawValue: sdkError.code) else { showErrorMessage("Unknown SDK error", error: error) return } if amityError == .globalBan { showErrorMessage("The current user is globally banned", error: error) } } ``` ```kotlin Android fun handleSdkError(exception: Throwable) { val amityError = AmityError.from(exception) if (amityError == AmityError.USER_IS_GLOBAL_BANNED) { print("The current user is globally banned.") } } ``` ```dart Flutter final Object error = AmityException( message: 'The current user is globally banned.', code: 400312, ); if (error is AmityException) { final amityError = error.toAmityError(); if (amityError == AmityError.USER_IS_GLOBAL_BANNED) { showError(error); } } ``` ## Common SDK Error Constants | Meaning | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Globally banned user | `Amity.ServerError.GLOBAL_BAN` | `.globalBan` | `AmityError.USER_IS_GLOBAL_BANNED` | `AmityError.USER_IS_GLOBAL_BANNED` | | Unauthorized session | `Amity.ServerError.UNAUTHORIZED` | `.unauthorized` | `AmityError.UNAUTHORIZED_ERROR` | `AmityError.UNAUTHORIZED_ERROR` | | Item not found | `Amity.ServerError.ITEM_NOT_FOUND` | `.itemNotFound` | `AmityError.ITEM_NOT_FOUND` | `AmityError.ITEM_NOT_FOUND` | | Permission denied | `Amity.ServerError.PERMISSION_DENIED` | `.permissionDenied` | `AmityError.PERMISSION_DENIED` | `AmityError.PERMISSION_DENIED` | | Visitor usage limit exceeded | `Amity.ServerError.VISITOR_USAGE_LIMIT_EXCEEDED` | `.visitorUsageLimitExceeded` | `AmityError.VISITOR_USAGE_LIMIT_EXCEEDED` | Not exposed as a public Flutter SDK enum in the current source | | Visitor permission denied | `Amity.ServerError.VISITOR_PERMISSION_DENIED` | `.visitorPermissionDenied` | `AmityError.VISITOR_PERMISSION_DENIED` | Not exposed as a public Flutter SDK enum in the current source | | Bot permission denied | `Amity.ServerError.BOT_PERMISSION_DENIED` | `.botPermissionDenied` | `AmityError.BOT_PERMISSION_DENIED` | Not exposed as a public Flutter SDK enum in the current source | | Max blocked users reached | `Amity.ServerError.MAX_BLOCKED_USERS_REACHED` (`400324`) | `.maxBlockedUsersReached` | `AmityError.MAX_BLOCKED_USERS_REACHED` | Refer to platform SDK | | Connection error | `Amity.ClientError.CONNECTION_ERROR` | `.connectionError` | `AmityError.CONNECTION_ERROR` | `AmityError.CONNECTION_ERROR` | ## Production Checklist - Disable broad network payload logging outside development builds. - Redact tokens, authorization headers, user identifiers, and message or post bodies before writing logs. - Keep retry logic bounded and avoid retrying authorization or global-ban errors. - Show user-friendly messages in the app UI; keep SDK codes in developer logs and support diagnostics. - Unsubscribe or dispose observers when the screen, debug session, or application lifecycle no longer needs them. --- ### [PII Detection](https://learn.social.plus/social-plus-sdk/core-concepts/safety-privacy/pii-detection) > Read and redact personally identifiable information metadata on supported SDK text objects. PII detection metadata is attached to text content when your network has PII detection enabled server-side. The native SDK helpers let apps read detected entities and produce redacted display text without changing the original post, comment, or message. This page covers the client SDK surface only. Feature enablement, quota, billing, and moderation-console behavior are product or backend concerns and are intentionally not documented here. ## SDK Surface | Platform | PII metadata helper | Redaction helper | Current SDK note | | --- | --- | --- | --- | | iOS | `getPIIData()` on `AmityPost`, `AmityComment`, and `AmityMessage` | `redactedText(piiCategories:replaceChar:)` on the same objects | Public helper API is available. | | Android | `getPIIData()` on `AmityPost`, `AmityComment`, and `AmityMessage` | `redactedText(piiCategories, replaceChar)` on `Data.TEXT` | Public helper API is available for text data. | | TypeScript | Not exposed in the current public SDK source | Not exposed | Use backend-provided data only if your integration has a separate API contract for it. | | Flutter | Not exposed in the current public SDK source | Not exposed | Use native SDK helpers through platform code only if your app owns that bridge. | ## Data Model Each detected entity is represented as a PII item. | Field | iOS type | Android type | Meaning | | --- | --- | --- | --- | | `offset` | `Int` | `Int` | Start index of the detected entity in the text. | | `length` | `Int` | `Int` | Number of characters in the detected entity. | | `category` | `AmityPIICategory` | `AmityPII.Category` | Normalized category when the SDK has one, or an `others` value for unknown categories. | | `confidence` | `Double` | `Double` | Detection confidence score from the server-provided metadata. | ## Categories The current native SDK helpers define dedicated categories for these values. | Category | iOS | Android | | --- | --- | --- | | Email | `.email` | `AmityPII.Category.EMAIL` | | Phone number | `.phoneNumber` | `AmityPII.Category.PHONE_NUMBER` | | IP address | `.ipAddress` | `AmityPII.Category.IP_ADDRESS` | | Address | `.address` | `AmityPII.Category.ADDRESS` | | Passport number | `.passportNumber` | `AmityPII.Category.PASSPORT_NUMBER` | | Unknown or newer category | `.others(value)` | `AmityPII.Category.OTHERS(value)` | Handle unknown categories deliberately. The server can introduce category strings before every client UI has a named enum case. ## Parameters | Parameter | iOS type | Android type | Default | Behavior | | --- | --- | --- | --- | --- | | `piiCategories` | `[AmityPIICategory]` | `List` | Empty list | Empty means redact every detected category. Provide categories to redact only selected types. | | `replaceChar` | `Character` | `Char` | `*` | Character used to replace each redacted character. | ## Read and Redact Use `getPIIData()` when you need entity metadata for moderation UI, highlighting, or audit views. Use `redactedText(...)` when the app should display masked text. ```swift iOS var cancellables = Set() let livePost = postRepository.getPost(withId: postId) livePost.$snapshot .sink { post in guard let post else { return } let piiItems = post.getPIIData() let redactedText = post.redactedText( piiCategories: [.email, .phoneNumber], replaceChar: "#" ) piiItems.forEach { pii in print("\(pii.category.value): \(pii.offset)-\(pii.length)") } showSuccessMessage(redactedText) } .store(in: &cancellables) let liveComment = commentRepository.getComment(withId: commentId) liveComment.$snapshot .sink { comment in guard let comment else { return } showSuccessMessage(comment.redactedText()) } .store(in: &cancellables) let liveMessage = messageRepository.getMessage(messageId) liveMessage.$snapshot .sink { message in guard let message else { return } showSuccessMessage(message.redactedText()) } .store(in: &cancellables) ``` ```kotlin Android import AmityPII val piiItems = post?.getPIIData().orEmpty() val redactedText = (post?.getData() as? AmityPost.Data.TEXT) ?.redactedText( piiCategories = listOf( AmityPII.Category.EMAIL, AmityPII.Category.PHONE_NUMBER ), replaceChar = '#' ) .orEmpty() piiItems.forEach { pii -> print("${pii.category}: ${pii.offset}-${pii.length}") } val redactedComment = (comment?.getData() as? AmityComment.Data.TEXT) ?.redactedText() .orEmpty() val redactedMessage = (message?.getData() as? AmityMessage.Data.TEXT) ?.redactedText() .orEmpty() print(redactedText + redactedComment + redactedMessage) ``` ## Behavior Notes - Redaction is non-destructive: it returns a string for display and does not change the stored text. - The native helpers skip PII ranges that overlap with mention ranges so user and channel mentions are not accidentally masked. - Metadata can be empty even when content is text. Treat an empty list as "no PII metadata on this object", not as a detection failure. - Android redaction is available on `Data.TEXT`; non-text post, comment, or message data returns no redacted text through this helper. - TypeScript and Flutter do not expose public PII helper methods in the current SDK source, so do not document `getPIIData()` or `redactedText()` for those SDKs. ## Core Concepts — Push Notifications ### [Device Registration](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/push-notifications/register-and-unregister-push-notifications-on-a-device) > Register and unregister a device for push notifications with the current SDK APIs. Device registration connects the current signed-in user to the device token that the operating system gives your app. Use it after your app has completed the platform push-notification setup and after the SDK client is initialized for a signed-in user. The TypeScript SDK does not expose a client-side device registration API. Use the native iOS, Android, or Flutter SDK APIs on device, and use server-side/webhook flows for web delivery. ## Platform Surface | Platform | Register | Unregister | Token argument | | --- | --- | --- | --- | | iOS | `client.registerPushNotification(withDeviceToken:)` | `client.unregisterPushNotification()` | `token: String` | | Android | `AmityCoreClient.registerPushNotification()` | `AmityCoreClient.unregisterPushNotification()` | None | | Flutter | `AmityCoreClient.registerDeviceNotification(fcmToken)` | `AmityCoreClient.unregisterDeviceNotification()` | `fcmToken: String` | | TypeScript | Not available | Not available | Not available | ## Parameters | Name | Platform | Type | Required | Description | | --- | --- | --- | --- | --- | | `token` | iOS | `String` | Yes | APNs device token passed to `registerPushNotification(withDeviceToken:)`. | | `fcmToken` | Flutter | `String` | Yes | FCM token passed to `registerDeviceNotification`. | Android registration does not accept a token argument in the current SDK API. The Android SDK registers the current signed-in user/device through `AmityCoreClient.registerPushNotification()`. ## Register A Device Register the current signed-in user/device after platform push setup and SDK initialization are complete. ```swift iOS try await client.registerPushNotification(withDeviceToken: "") ``` ```kotlin Android AmityCoreClient.registerPushNotification() .doOnComplete { // Device registration completed. } .doOnError { error -> // Handle registration error. } .subscribe() ``` ```dart Flutter await AmityCoreClient.registerDeviceNotification(fcmToken); ``` On Android, `registerPushNotification()` is a no-op for visitor and bot users. Register only after the app has a signed-in user session. ## Unregister A Device Call unregister when the user signs out or when this app installation should stop receiving push notifications for the currently registered user. ```swift iOS try await client.unregisterPushNotification() ``` ```kotlin Android AmityCoreClient.unregisterPushNotification() .doOnComplete { // Device unregistration completed. } .doOnError { error -> // Handle unregistration error. } .subscribe() ``` ```dart Flutter await AmityCoreClient.unregisterDeviceNotification(); ``` ## Related Setup Complete platform setup before calling the SDK registration API: - [iOS Setup](./setup/ios-setup) - [Android Setup](./setup/android-setup) - [Flutter Setup](./setup/flutter-setup) --- ### [Push Notification Settings](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/push-notifications/settings/overview) > Choose which push notifications a user receives at user, channel, and community scope. Push notification settings are separate from device registration. Device registration tells social.plus which app installation can receive push notifications. Settings control which push notifications the signed-in user wants to receive. ## Settings Scopes | Scope | What it controls | SDK entrypoint | | --- | --- | --- | | User | Account-wide push preferences, with optional module modifiers for chat, social, and video streaming. | `notifications().user()` / `client.notificationManager` | | Channel | Push preference for one chat channel. | `notifications().channel(channelId)` / channel notification manager | | Community | Push preference for one community, with optional event modifiers. | `notifications().community(communityId)` / community notification manager | The SDK exposes these scopes independently. Read and update the exact scope your product setting is changing instead of assuming a hidden precedence rule. ## Platform Availability | Platform | User settings | Channel settings | Community settings | | --- | --- | --- | --- | | TypeScript | Yes | Yes | Yes | | iOS | Yes | Yes | Yes | | Android | Yes | Yes | Yes | | Flutter | Yes | Yes | Yes | ## Guides Configure account-wide push preferences and module modifiers. Enable, disable, or read push settings for a channel. Configure community push settings and event modifiers. ## Common Model | Concept | Meaning | | --- | --- | | `isEnabled` | Whether push notifications are enabled at the requested scope. | | Module modifier | User-level setting for chat, social, or video-streaming notifications. | | Event modifier | Community-level setting for post, comment, story, and platform-supported livestream events. | | Role filter | Optional filter that limits notification delivery by sender role where the SDK supports modifiers. | --- ### [User Notification Settings](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/push-notifications/settings/user-settings) > Read and update account-wide push notification settings for the signed-in user. User notification settings apply across the signed-in user's devices. Use this scope for global push preferences and module-level choices for chat, social, and video-streaming notifications. ## API surface | Platform | Manager | Read | Enable | Disable all | | --- | --- | --- | --- | --- | | TypeScript | `Client.notifications().user()` | `getSettings()` | `enable(modules?)` | `disableAllNotifications()` | | iOS | `client.notificationManager` | `getSettings()` | `enable(for:)` | `disableAllNotifications()` | | Android | `AmityCoreClient.notifications().user()` | `getSettings()` | `enable(moduleModifiers?)` | `disableAllNotifications()` | | Flutter | `AmityCoreClient.notifications().user()` | `getSettings()` | `enable(eventModifiers)` | `disable()` | ## Parameters | Name | Platform | Type | Required | Description | | --- | --- | --- | --- | --- | | `modules` | TypeScript | `Amity.UserNotificationModule[]` | No | Module-level settings to send with `enable`. | | `modules` | iOS | `[AmityUserNotificationModule]?` | No | Module-level settings passed to `enable(for:)`. | | `moduleModifiers` | Android | `List?` | No | Module modifiers passed to `enable`. | | `eventModifiers` | Flutter | `List?` | Nullable | Module modifiers passed to `enable`. | | `roleFilter` | TypeScript, iOS, Android, Flutter | Role filter | No | Optional filter for receiving notifications only from matching sender roles. | ## Modules | Module value | TypeScript enum | iOS case | Android/Flutter modifier | | --- | --- | --- | --- | | `chat` | `CHAT` | `.chat` | `CHAT` / `Chat` | | `social` | `SOCIAL` | `.social` | `SOCIAL` / `Social` | | `video-streaming` | `VIDEO_STREAMING` | `.videoStreaming` | `VIDEO_STREAMING` / `VideoStreaming` | ## Get user settings Read account-wide notification settings before rendering global or module-level push controls. ```typescript TypeScript import { Client, NotificationRolesFilterTypeEnum } from '@amityco/ts-sdk'; const settings = await Client.notifications() .user() .getSettings(); const isEnabled = settings.isEnabled; settings.modules.forEach(module => { const moduleName = module.moduleName; const moduleEnabled = module.isEnabled; if (module.rolesFilter?.type === NotificationRolesFilterTypeEnum.ONLY) { const roleIds = module.rolesFilter.roleIds; updateUI({ moduleName, moduleEnabled, roleIds }); } }); ``` ```swift iOS let notificationManager = client.notificationManager let settings = try await notificationManager.getSettings() let isEnabled = settings.isEnabled for module in settings.modules { switch module.moduleType { case .chat: print("chat enabled: \(module.isEnabled)") case .social: print("social enabled: \(module.isEnabled)") case .videoStreaming: print("video streaming enabled: \(module.isEnabled)") @unknown default: print("unknown module enabled: \(module.isEnabled)") } } ``` ```kotlin Android AmityCoreClient.notifications() .user() .getSettings() .doOnSuccess { settings -> val isEnabled = settings.isEnabled() settings.getModules()?.forEach { module -> when (module) { is AmityUserNotificationModule.CHAT -> { val moduleEnabled = module.isEnabled() } is AmityUserNotificationModule.SOCIAL -> { val moduleEnabled = module.isEnabled() val rolesFilter = module.getRolesFilter() } is AmityUserNotificationModule.VIDEO_STREAMING -> { val moduleEnabled = module.isEnabled() } else -> Unit } } } .doOnError { error -> // Handle error. } .subscribe() ``` ```dart Flutter final settings = await AmityCoreClient .notifications() .user() .getSettings(); final isEnabled = settings.isEnabled; settings.events?.forEach((module) { final moduleEnabled = module.isEnabled; final rolesFilter = module.rolesFilter; }); ``` ## Update user settings Enable, disable, or customize account-wide notification modules after the user changes global push preferences. ```typescript TypeScript import { Client, NotificationRolesFilterTypeEnum, UserNotificationModuleNameEnum, } from '@amityco/ts-sdk'; await Client.notifications() .user() .enable([ { moduleName: UserNotificationModuleNameEnum.CHAT, isEnabled: true, }, { moduleName: UserNotificationModuleNameEnum.SOCIAL, isEnabled: true, rolesFilter: { type: NotificationRolesFilterTypeEnum.ONLY, roleIds: ['community-moderator'], }, }, { moduleName: UserNotificationModuleNameEnum.VIDEO_STREAMING, isEnabled: false, }, ]); await Client.notifications() .user() .disableAllNotifications(); ``` ```swift iOS let notificationManager = client.notificationManager try await notificationManager.enable(for: [ AmityUserNotificationModule( moduleType: .chat, isEnabled: true, roleFilter: nil ), AmityUserNotificationModule( moduleType: .social, isEnabled: true, roleFilter: AmityRoleFilter.onlyFilter(withRoleIds: ["community-moderator"]) ), AmityUserNotificationModule( moduleType: .videoStreaming, isEnabled: false, roleFilter: nil ), ]) try await notificationManager.disableAllNotifications() ``` ```kotlin Android val rolesFilter = AmityRolesFilter.ONLY(AmityRoles(listOf("community-moderator"))) val chatModifier = AmityUserNotificationModule.CHAT.enable() val socialModifier = AmityUserNotificationModule.SOCIAL.enable(rolesFilter) val videoStreamingModifier = AmityUserNotificationModule.VIDEO_STREAMING.disable() AmityCoreClient.notifications() .user() .enable( moduleModifiers = listOf( chatModifier, socialModifier, videoStreamingModifier ) ) .doOnComplete { // User notification settings updated. } .doOnError { error -> // Handle error. } .subscribe() AmityCoreClient.notifications() .user() .disableAllNotifications() .subscribe() ``` ```dart Flutter final rolesFilter = Only(AmityRoles(roles: ['community-moderator'])); await AmityCoreClient .notifications() .user() .enable([ Chat.enable(null), Social.enable(rolesFilter), VideoStreaming.disable(), ]); await AmityCoreClient .notifications() .user() .disable(); ``` ## Related Configure push settings for a chat channel. Configure push settings for social community events. --- ### [Channel Notification Settings](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/push-notifications/settings/channel-settings) > Read and update push notification settings for a single chat channel. Channel notification settings control whether the signed-in user receives push notifications for one channel. ## API surface | Platform | Manager | Read | Enable | Disable | | --- | --- | --- | --- | --- | | TypeScript | `Client.notifications().channel(channelId)` | `getSettings()` | `enable()` | `disable()` | | iOS | `channelRepository.notificationManagerForChannel(withId:)` | `getSettings()` | `enable()` | `disable()` | | Android | `AmityCoreClient.notifications().channel(channelId)` | `getSettings()` | `enable()` | `disable()` | | Flutter | `AmityCoreClient.notifications().channel(channelId)` | `getSettings()` | `enable()` | `disable()` | ## Parameters | Name | Platform | Type | Required | Description | | --- | --- | --- | --- | --- | | `channelId` | TypeScript, iOS, Android, Flutter | `String` / `string` | Yes | ID of the channel whose push setting should be read or updated. | ## Get channel settings Read channel notification settings before rendering a channel-level push toggle. ```typescript TypeScript import { Client } from '@amityco/ts-sdk'; const settings = await Client.notifications() .channel(channelId) .getSettings(); const isEnabled = settings.isEnabled; ``` ```swift iOS let notificationManager = channelRepository.notificationManagerForChannel(withId: channelId) let settings = try await notificationManager.getSettings() let isEnabled = settings.isEnabled ``` ```kotlin Android AmityCoreClient.notifications() .channel(channelId) .getSettings() .doOnSuccess { settings: AmityChannelNotificationSettings -> val isEnabled = settings.isEnabled() } .doOnError { error -> // Handle error. } .subscribe() ``` ```dart Flutter final settings = await AmityCoreClient .notifications() .channel(channelId) .getSettings(); final isEnabled = settings.isEnabled; ``` ## Update channel settings Enable or disable notifications for a channel after the user changes that channel's push preference. ```typescript TypeScript import { Client } from '@amityco/ts-sdk'; await Client.notifications() .channel(channelId) .enable(); await Client.notifications() .channel(channelId) .disable(); ``` ```swift iOS let notificationManager = channelRepository.notificationManagerForChannel(withId: channelId) try await notificationManager.enable() try await notificationManager.disable() ``` ```kotlin Android AmityCoreClient.notifications() .channel(channelId) .enable() .doOnComplete { // Channel push notifications enabled. } .doOnError { error -> // Handle error. } .subscribe() AmityCoreClient.notifications() .channel(channelId) .disable() .doOnComplete { // Channel push notifications disabled. } .doOnError { error -> // Handle error. } .subscribe() ``` ```dart Flutter await AmityCoreClient .notifications() .channel(channelId) .enable(); await AmityCoreClient .notifications() .channel(channelId) .disable(); ``` ## Related Configure account-wide push preferences. Configure push settings for social community events. --- ### [Community Notification Settings](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/push-notifications/settings/community-settings) > Read and update push notification settings for one community. Community notification settings control whether the signed-in user receives push notifications from one community. You can also pass event modifiers to enable or disable specific community notification events. ## API surface | Platform | Manager | Read | Enable | Disable | | --- | --- | --- | --- | --- | | TypeScript | `Client.notifications().community(communityId)` | `getSettings()` | `enable(events?)` | `disable()` | | iOS | `communityRepository.notificationManager(forCommunityId:)` | `getSettings()` | `enable(events:)` | `disable()` | | Android | `AmityCoreClient.notifications().community(communityId)` | `getSettings()` | `enable(eventModifiers?)` | `disable()` | | Flutter | `AmityCoreClient.notifications().community(communityId)` | `getSettings()` | `enable(eventModifiers)` | `disable()` | ## Parameters | Name | Platform | Type | Required | Description | | --- | --- | --- | --- | --- | | `communityId` | TypeScript, iOS, Android, Flutter | `String` / `string` | Yes | ID of the community whose push setting should be read or updated. | | `events` | TypeScript, iOS | Community notification event list | No on TypeScript, yes on iOS | Event-level modifiers to send with `enable`. | | `eventModifiers` | Android, Flutter | Community notification modifier list | No on Android, nullable on Flutter | Event-level modifiers to send with `enable`. | | `roleFilter` | TypeScript, iOS, Android, Flutter | Role filter | No | Optional filter for receiving notifications only from matching sender roles. | ## Community events | Event value | TypeScript enum | iOS case | Android modifier | Flutter modifier | | --- | --- | --- | --- | --- | | `post.created` | `POST_CREATED` | `.postCreated` | `POST_CREATED` | `PostCreated` | | `post.reacted` | `POST_REACTED` | `.postReacted` | `POST_REACTED` | `PostReacted` | | `comment.created` | `COMMENT_CREATED` | `.commentCreated` | `COMMENT_CREATED` | `CommentCreated` | | `comment.replied` | `COMMENT_REPLIED` | `.commentReplied` | `COMMENT_REPLIED` | `CommentReplied` | | `comment.reacted` | `COMMENT_REACTED` | `.commentReacted` | `COMMENT_REACTED` | `CommentReacted` | | `story.created` | `STORY_CREATED` | `.storyCreated` | `STORY_CREATED` | `StoryCreated` | | `story.reacted` | `STORY_REACTED` | `.storyReacted` | `STORY_REACTED` | `StoryReacted` | | `story-comment.created` | `STORY_COMMENT_CREATED` | `.storyCommentCreated` | `STORY_COMMENT_CREATED` | `StoryCommentCreated` | | `video-streaming.didStart` | `LIVESTREAM_START` | `.livestreamStart` | `LIVESTREAM_START` | Not exposed | Flutter exposes `video-streaming` as a user notification module, but this checkout does not expose a community-level livestream-start event modifier. ## Get community settings Read community notification settings before rendering community-level or event-level push controls. ```typescript TypeScript import { Client } from '@amityco/ts-sdk'; const settings = await Client.notifications() .community(communityId) .getSettings(); const isEnabled = settings.isEnabled; settings.events.forEach(event => { const eventName = event.eventName; const eventEnabled = event.isEnabled; const networkEnabled = event.isNetworkEnabled; updateUI({ eventName, eventEnabled, networkEnabled }); }); ``` ```swift iOS let notificationManager = communityRepository.notificationManager(forCommunityId: communityId) let settings = try await notificationManager.getSettings() let isEnabled = settings.isEnabled for event in settings.events { print("- event \(event.eventName) enabled: \(event.isEnabled)") } ``` ```kotlin Android AmityCoreClient.notifications() .community(communityId) .getSettings() .doOnSuccess { settings: AmityCommunityNotificationSettings -> val isEnabled = settings.isEnabled() settings.getNotificationEvents().forEach { event -> when (event) { is AmityCommunityNotificationEvent.POST_CREATED -> { val eventEnabled = event.isEnabled() val networkEnabled = event.isNetworkEnabled() } else -> Unit } } } .doOnError { error -> // Handle error. } .subscribe() ``` ```dart Flutter final settings = await AmityCoreClient .notifications() .community(communityId) .getSettings(); final isEnabled = settings.isEnabled; settings.events?.forEach((event) { final eventEnabled = event.isEnabled; final networkEnabled = event.isNetworkEnabled; }); ``` ## Update community settings Enable, disable, or customize community notification events after the user changes community push preferences. ```typescript TypeScript import { Client, CommunityNotificationEventNameEnum, NotificationRolesFilterTypeEnum, } from '@amityco/ts-sdk'; await Client.notifications() .community(communityId) .enable([ { eventName: CommunityNotificationEventNameEnum.POST_CREATED, isEnabled: true, rolesFilter: { type: NotificationRolesFilterTypeEnum.ONLY, roleIds: ['community-moderator'], }, }, { eventName: CommunityNotificationEventNameEnum.STORY_REACTED, isEnabled: false, }, ]); await Client.notifications() .community(communityId) .disable(); ``` ```swift iOS let notificationManager = communityRepository.notificationManager(forCommunityId: communityId) try await notificationManager.enable(events: [ AmityCommunityNotificationEvent( eventType: .postCreated, isEnabled: true, roleFilter: AmityRoleFilter.onlyFilter(withRoleIds: ["community-moderator"]) ), AmityCommunityNotificationEvent( eventType: .storyReacted, isEnabled: false, roleFilter: nil ), ]) try await notificationManager.disable() ``` ```kotlin Android val rolesFilter = AmityRolesFilter.ONLY(AmityRoles(listOf("community-moderator"))) val postCreatedModifier = AmityCommunityNotificationEvent.POST_CREATED.enable(rolesFilter) val storyReactedModifier = AmityCommunityNotificationEvent.STORY_REACTED.disable() AmityCoreClient.notifications() .community(communityId) .enable( eventModifiers = listOf( postCreatedModifier, storyReactedModifier ) ) .doOnComplete { // Community notification settings updated. } .doOnError { error -> // Handle error. } .subscribe() AmityCoreClient.notifications() .community(communityId) .disable() .subscribe() ``` ```dart Flutter final rolesFilter = Only(AmityRoles(roles: ['community-moderator'])); await AmityCoreClient .notifications() .community(communityId) .enable([ PostCreated.enable(rolesFilter), StoryReacted.disable(), ]); await AmityCoreClient .notifications() .community(communityId) .disable(); ``` ## Related Configure account-wide push preferences and module modifiers. Configure push settings for a chat channel. --- ### [Android Push Notifications](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/android-setup) > Connect Android push tokens to the current social.plus Android SDK. Android push setup has two separate responsibilities: 1. Configure the Android app and provider, such as Firebase Cloud Messaging. 2. Connect the provider token and signed-in device to social.plus through the SDK. Push delivery also requires server-side credentials in the social.plus Console. The client SDK can register a device only after the platform push provider and console configuration are complete. ## SDK Surface | Purpose | Current Android API | When to call | | --- | --- | --- | | Store an FCM token with the SDK push adapter | `AmityFcm.create().setup(fcmToken)` | When Firebase gives your app an FCM token, including token refresh. | | Register the current signed-in device/user | `AmityCoreClient.registerPushNotification()` | After the SDK has a signed-in user session. | | Unregister the current device/user | `AmityCoreClient.unregisterPushNotification()` | On sign-out or when this installation should stop receiving push notifications. | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `fcmToken` | `String` | Yes | The FCM registration token returned by Firebase for this app installation. | | `baiduApiKey` | `String` | Only for Baidu builds | API key passed to the optional Baidu push adapter when that module is included in your Android build. | ## 1. Configure FCM Follow the Android and Firebase setup flow for your app: - Add `google-services.json` to your app module. - Apply the Google Services Gradle plugin. - Add Firebase Messaging using the version recommended for your Firebase setup. - Request `POST_NOTIFICATIONS` at runtime on Android 13+ when your product needs visible notifications. If your Android dependency setup installs SDK modules separately, include the FCM push adapter with the same version as your social.plus Android SDK: ```gradle Gradle dependencies { implementation 'co.amity.android:amity-push-fcm:x.y.z' } ``` Use the same `x.y.z` version you use for `co.amity.android:amity-sdk`. If your package already bundles the FCM adapter, do not add a duplicate dependency. ## 2. Upload Provider Credentials In the social.plus Console, open **Settings > Push Notifications** and upload the Firebase service account JSON for your app. Without this server-side credential, client registration can succeed while push delivery still fails. ## 3. Connect the FCM Token Pass every current FCM token to the SDK push adapter. Do this for the initial token and every token refresh. ```kotlin Android import com.amity.socialcloud.sdk.push.AmityFcm val fcmToken = "fcm-token" AmityFcm.create() .setup(fcmToken) .subscribe() ``` The Android sample app calls the same adapter from `FirebaseMessagingService.onNewToken(token)`. ## 4. Register the Signed-In Device After the SDK has a signed-in user session and the app has connected the FCM token, register the current device/user with social.plus. ```kotlin Android AmityCoreClient.registerPushNotification() .doOnComplete { // Device registration completed. } .doOnError { error -> // Handle registration error. } .subscribe() ``` Call unregister when the user signs out or disables push for this installation. ```kotlin Android AmityCoreClient.unregisterPushNotification() .doOnComplete { // Device unregistration completed. } .doOnError { error -> // Handle unregistration error. } .subscribe() ``` `registerPushNotification()` has no token parameter in the current Android SDK. The token is handled by the push adapter through `AmityFcm.create().setup(fcmToken)`. ## Optional Baidu Adapter The Android source also contains an optional Baidu push adapter, `AmityBaidu.create(context).setup(baiduApiKey)`, for builds that include the Baidu push module. The default Android SDK build path uses the FCM adapter, so treat Baidu setup as a China-market build decision and verify the module distribution before enabling it. ## Setup Checklist - FCM is configured in the Android app. - The Firebase service account JSON is uploaded in the social.plus Console. - The latest FCM token is passed to `AmityFcm.create().setup(fcmToken)`. - The signed-in device/user is registered with `AmityCoreClient.registerPushNotification()`. - The device is unregistered on sign-out when the app should stop receiving push notifications for that user. --- ### [Flutter Push Notification Setup](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/flutter-setup) > Register Flutter push tokens with the current social.plus Flutter SDK. Flutter push setup combines native platform configuration with one SDK registration call. Your app obtains the push token through Firebase or the native platform, then passes that token to the social.plus Flutter SDK after the user is signed in. The Flutter SDK does not create Firebase projects, APNs certificates, or platform permission prompts for you. It registers the token that your app receives from those platform systems. ## SDK Surface | Purpose | Current Flutter API | When to call | | --- | --- | --- | | Register the current device token | `AmityCoreClient.registerDeviceNotification(fcmToken)` | After the SDK has a signed-in user session and your app has a current token. | | Unregister the current device | `AmityCoreClient.unregisterDeviceNotification()` | On sign-out or when this installation should stop receiving push notifications. | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `fcmToken` | `String` | Yes | The token your Flutter app receives from Firebase Messaging or the platform push provider. | ## 1. Configure Platform Push Configure Firebase Messaging for Flutter using your app's Firebase project: - Add the Firebase packages that match your FlutterFire setup. - Run FlutterFire configuration for Android and iOS. - Add `google-services.json` for Android. - Add `GoogleService-Info.plist` for iOS. - Enable Push Notifications capability for the iOS target. - Upload the required push credentials in the social.plus Console under **Settings > Push Notifications**. Keep Firebase and FlutterFire package versions in your app, not hardcoded in this docs page. Follow the version matrix for your Flutter, Firebase, and React Native toolchain. ## 2. Register the Token Once your app has a token and the user is signed in to social.plus, register the device token with the SDK. ```dart Flutter final fcmToken = ''; await AmityCoreClient.registerDeviceNotification(fcmToken); ``` Call the same SDK method again when Firebase reports a refreshed token. The last registration replaces the previous token state for this app installation. ## 3. Unregister on Sign-Out Unregister when the user signs out or when this app installation should stop receiving push notifications for the current social.plus user. ```dart Flutter await AmityCoreClient.unregisterDeviceNotification(); ``` ## Setup Checklist - Firebase Messaging is configured for the Flutter app. - Android and iOS platform files are present in the correct native projects. - APNs or FCM credentials are uploaded in the social.plus Console. - The signed-in user token is registered through `AmityCoreClient.registerDeviceNotification(fcmToken)`. - The device is unregistered through `AmityCoreClient.unregisterDeviceNotification()` on sign-out. --- ### [iOS Push Notifications](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/ios-setup) > Register APNs device tokens with the current social.plus iOS SDK. iOS push setup has two parts: configure APNs for your app, then register the APNs device token with the social.plus iOS SDK after the user is signed in. Push notifications require an Apple Developer account, the Push Notifications capability, and valid APNs credentials uploaded in the social.plus Console. ## SDK Surface | Purpose | Current iOS API | When to call | | --- | --- | --- | | Register the current device token | `client.registerPushNotification(withDeviceToken:)` | After APNs returns a device token and the SDK has a signed-in user session. | | Unregister the current device | `client.unregisterPushNotification()` | On sign-out or when this installation should stop receiving push notifications. | ## Parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `token` | `String` | Yes | APNs device token converted from the `Data` value returned by iOS. | ## 1. Configure APNs In Apple Developer Console: 1. Open **Certificates, Identifiers & Profiles**. 2. Select your app's Bundle ID. 3. Enable Push Notifications. 4. Create the APNs certificate or auth key your release process uses. In Xcode: 1. Select your app target. 2. Open **Signing & Capabilities**. 3. Add **Push Notifications**. 4. Add background remote-notification mode only if your app needs background notification handling. In the social.plus Console, open **Settings > Push Notifications** and upload the iOS APNs credential for the same Bundle ID. ## 2. Request System Registration Ask the user for notification permission at a product moment that makes sense, then ask iOS to register the app for remote notifications. ```swift iOS import UserNotifications UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } ``` ## 3. Register the APNs Token When iOS calls your app delegate with the device token, convert the token data to a hex string and pass it to the SDK. ```swift iOS func application( _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data ) { let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() Task { @MainActor in do { try await client.registerPushNotification(withDeviceToken: token) } catch { handleError(error) } } } ``` `registerPushNotification(withDeviceToken:)` is available for signed-in users. The SDK throws for guest users. ## 4. Unregister on Sign-Out Call unregister when the current app installation should stop receiving push notifications for the signed-in social.plus user. ```swift iOS try await client.unregisterPushNotification() ``` ## Setup Checklist - Push Notifications capability is enabled for the app target. - The APNs credential for the app Bundle ID is uploaded in the social.plus Console. - The app calls `UIApplication.shared.registerForRemoteNotifications()` after permission is granted. - The APNs device token is passed to `client.registerPushNotification(withDeviceToken:)`. - The app calls `client.unregisterPushNotification()` on sign-out when push should stop for that user. --- ### [React Native Push Notifications](https://learn.social.plus/social-plus-sdk/core-concepts/realtime-communication/push-notifications/setup/react-native-setup) > Understand React Native push-notification support in the SDK docs. The TypeScript SDK does not currently expose a client-side push device registration API. That means a React Native app cannot complete social.plus push registration by calling a TypeScript SDK method from JavaScript. Do not build React Native push setup around a TypeScript SDK method such as `registerPushNotification()` or `registerDeviceNotification()`. Those APIs are not exported by `@amityco/ts-sdk`. ## Current Support Boundary | Layer | Status | What to use | | --- | --- | --- | | React Native JavaScript with `@amityco/ts-sdk` | No client device registration API | Use the TypeScript SDK for supported chat and social data features, not push device registration. | | Android native SDK | Supported | Use [Android Push Notifications](./android-setup) and call the native Android SDK registration APIs. | | iOS native SDK | Supported | Use [iOS Push Notifications](./ios-setup) and call the native iOS SDK registration APIs. | | Flutter SDK | Supported | Use [Flutter Push Notification Setup](./flutter-setup) for Flutter apps. | ## Integration Options For a React Native app that needs social.plus push notifications, choose one of these supported paths: 1. Implement push registration in native Android and iOS layers using the native SDKs. 2. Bridge the native registration result into React Native only for app state or UI, not as a TypeScript SDK call. 3. If you use a React Native UI Kit package, follow the UI Kit's push-notification instructions for that package. UI Kit APIs are outside this SDK-first audit. ## Console Configuration Still Applies Even when registration happens through native code, the app still needs provider credentials in the social.plus Console: - Android builds need the Firebase service account JSON uploaded under **Settings > Push Notifications**. - iOS builds need the APNs credential for the app Bundle ID uploaded under **Settings > Push Notifications**. ## Related Setup Configure FCM and register the signed-in device through the Android SDK. Configure APNs and register the device token through the iOS SDK. Compare device registration APIs across supported SDK platforms. ## Video — Getting Started ### [Move from Stream to Room](https://learn.social.plus/social-plus-sdk/video-new/migration-guide) > Plan SDK code changes when moving livestream integrations from legacy Stream APIs to room-based live experiences. Use room APIs for new livestream and co-host experiences. Legacy Stream APIs still exist in the audited SDK sources, so treat this page as an application migration checklist rather than a removal timeline or data-migration promise. This page focuses on SDK code paths only. UIKit, Admin Console, backend API behavior, commercial timelines, and product analytics dashboards are outside this SDK-first migration guide. ## Migration Scope | Area | Keep legacy Stream support for | Move new work to Room APIs | | --- | --- | --- | | Existing content | Existing Stream posts and playback screens that your product still needs to support | New live room and co-host experiences | | Entity model | `AmityStream` / `streamId` | `AmityRoom` / `roomId` | | Feed post type | Legacy live stream post data | Room post data with `roomId` | | Playback source | Stream watcher or recording data | `livePlaybackUrl` and `recordedPlaybackInfos` / `recordedData` / `getRecordedPlaybackInfos()` | | Watch analytics | Legacy stream-session logic where still used | `room.analytics()` watch-session APIs | Do not delete legacy Stream handling until every screen that can display old Stream posts has a fallback path. A migration usually means supporting both old Stream content and new Room content during rollout. ## Platform Surface | Platform | Legacy Stream surface found | Room surface found | Room post creation | Migration note | | --- | --- | --- | --- | --- | | TypeScript | `StreamRepository`, `Amity.Stream`, `streamId` fields in legacy surfaces | `RoomRepository`, `Amity.Room` | `PostRepository.createRoomPost()` | Use room APIs for new rooms and keep stream handling for old posts. | | iOS | `AmityStreamRepository`, `AmityStream` | `AmityRoomRepository`, `AmityRoom` | `AmityPostRepository.createRoomPost()` with `AmityRoomPostBuilder` | Retain `AmityNotificationToken` values while observing live objects or collections. | | Android | `AmityVideoClient.newStreamRepository()`, `AmityStreamRepository`, `AmityStream` | `AmityVideoClient.newRoomRepository()`, `AmityRoomRepository`, `AmityRoom` | `AmityPostRepository.createRoomPost()` | Room APIs return Rx types; dispose subscriptions with your app lifecycle. | | Flutter | Legacy `AmityVideoClient.newStreamRepository()` and `StreamRepository` | No current public room broadcasting repository found in this audit | No current public room post creator found in this audit | Do not promise a Flutter room migration path from SDK docs until the public API exists. | ## Concept Mapping | Legacy Stream concept | Room-based concept | What changes in app code | | --- | --- | --- | | `streamId` | `roomId` | Store and pass room IDs for new room content. Keep stream IDs for legacy content. | | `AmityStream` | `AmityRoom` | Read room status, participants, playback URLs, and recorded metadata from room models. | | Stream repository | Room repository | Use `RoomRepository` / `AmityRoomRepository` / `newRoomRepository()` for new room flows. | | Legacy live stream post | Room post | Create a room first, then create a room post that references the `roomId`. | | Stream watcher URL / recordings | Room live and recorded playback fields | Pass SDK-returned room playback URLs to your app-owned player. | | Stream session analytics | Room watch-session analytics | Use `room.analytics()` for new room watch sessions. | ## Parameters | Operation | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | --- | | Observe room content | `roomId` | Yes | TypeScript, iOS, Android | Room ID used when replacing new Stream observation flows with room observation. | | Create room | Room options | Yes | TypeScript, iOS, Android | Room title and configuration passed to the room repository. | | Publish room post | `communityId` or target ID | Yes | TypeScript, iOS, Android | Feed target where the room post should be published. | | Publish room post | `roomId` | Yes | TypeScript, iOS, Android | Room ID returned by room creation and embedded in the room post. | | Publish room post | Text or title fields | No | TypeScript, iOS, Android | Display text used by the feed post that references the room. | ## Observe Room Content Replace new Stream observation flows with room observation for new room-based screens. Each snippet below is standalone and uses the current room repository surface. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; function observeRoomForMigration(roomId: string): Amity.Unsubscriber { return RoomRepository.getRoom(roomId, snapshot => { if (snapshot.error) { handleError(snapshot.error); return; } const room = snapshot.data; showSuccessMessage(room.status); }); } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient val disposable = AmityVideoClient.newRoomRepository() .getRoom(roomId) .subscribe( { room -> showSuccessMessage(room.getStatus()) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS var roomObservationToken: AmityNotificationToken? let roomObject = AmityRoomRepository().getRoom(withId: roomId) roomObservationToken = roomObject.observe { liveObject, error in if let error { handleGeneralError(error) return } if let room = liveObject.snapshot { showSuccessMessage(room.status.rawValue) } } ``` ## Create and Publish a Room For new room-based livestreams, create the room first. Publish it into a user or community feed by creating a room post with the returned `roomId`. ```typescript TypeScript import { PostRepository, RoomRepository } from "@amityco/ts-sdk"; async function createAndPublishRoom(communityId: string): Promise { const { data: room } = await RoomRepository.createRoom({ title: "Live room", liveChatEnabled: true, type: "coHosts", }); const { data: post } = await PostRepository.createRoomPost({ targetType: "community", targetId: communityId, data: { roomId: room.roomId, text: "Join this live room", title: "Live room", }, }); showSuccessMessage(post.postId); return post.postId; } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.social.AmitySocialClient import com.amity.socialcloud.sdk.api.video.AmityVideoClient import com.amity.socialcloud.sdk.model.social.post.AmityPost val postRepository = AmitySocialClient.newPostRepository() val disposable = AmityVideoClient.newRoomRepository() .createRoom( title = "Live room", liveChatEnabled = true ) .flatMap { room -> postRepository.createRoomPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, roomId = room.getRoomId(), text = "Join this live room", title = "Live room" ) } .subscribe( { post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS func createAndPublishRoom(communityId: String) async throws -> String { let options = AmityRoomCreateOptions( title: "Live room", liveChatEnabled: true, type: .coHosts ) let room = try await AmityRoomRepository().createRoom(with: options) let builder = AmityRoomPostBuilder( roomId: room.roomId, text: "Join this live room" ) builder.setTitle("Live room") let post = try await AmityPostRepository().createRoomPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil ) showSuccessMessage(post.postId) return post.postId } ``` ## Migration Checkpoints | Checkpoint | What to verify | | --- | --- | | Data model | New room-backed records store `roomId`; legacy records that still point to streams keep `streamId`. | | Feed rendering | Feed cards branch by data type and render both legacy Stream posts and new Room posts during rollout. | | Playback | New Room playback reads `livePlaybackUrl` for live rooms and recorded metadata only after `recorded` status. | | Broadcasting | New broadcasts create a room and request broadcaster data before connecting your media layer. | | Chat | If your room enables live chat, fetch the room live chat channel through the room API before rendering chat UI. | | Analytics | Start room watch sessions only for `live` or `recorded` rooms and update watch duration from player state. | | Lifecycle | Retain and dispose each platform's subscription/token/disposable with the screen lifecycle. | ## Rollout Strategy 1. Add room post rendering while keeping legacy Stream post rendering. 2. Create new livestreams through room APIs and create room posts for feed visibility. 3. Move playback screens to read room playback fields for room posts. 4. Wire room watch analytics after the player starts active playback. 5. Keep legacy Stream code only for content that still exists in your product. Room APIs do not make the SDK own camera capture, player controls, autoplay, buffering, captions, or DRM. Those remain app and media-player responsibilities. ## Related Topics Create rooms and retrieve broadcaster data. Publish an existing room into a feed. Choose live or recorded room playback sources. ## Video — Broadcasting ### [Broadcasting Overview](https://learn.social.plus/social-plus-sdk/video-new/broadcasting/overview) > SDK-backed overview of room-based live broadcasting and co-host rooms. Room broadcasting is the current SDK path for live rooms, including co-host rooms and direct-streaming room types where the platform exposes them. Treat rooms as the social.plus record for the live session: the SDK creates, observes, updates, stops, and publishes room objects, while your app's media stack connects to the streaming provider with credentials returned by the SDK. This page is about the SDK room APIs. The legacy stream APIs and UIKit-level video experiences are separate surfaces. ## Platform Surface | Platform | Current SDK surface | Notes | | --- | --- | --- | | TypeScript | `RoomRepository` and `PostRepository.createRoomPost()` | Supports room creation, live object/live collection observers, broadcaster data, stop/update/delete, recorded URL lookup, and room posts. | | iOS | `AmityRoomRepository`, `AmityRoomCreateOptions`, and `AmityPostRepository.createRoomPost()` | Supports room creation, `AmityObject`, `AmityCollection`, stop/update/delete, co-host events, room token generation, and room posts. | | Android | `AmityVideoClient.newRoomRepository()` and `AmitySocialClient.newPostRepository().createRoomPost()` | Supports room creation, `Flowable`, paging room queries, stop/update/delete, broadcaster data, recorded URLs, co-host events, and room posts. | | Flutter | No current public room broadcasting repository found in this audit | The Flutter source has older stream APIs such as `AmityStream`, but no public `AmityRoomRepository` or room post creation surface. | ## What the SDK Owns | Concern | SDK-owned | App-owned | | --- | --- | --- | | Room record | Create, read, query, update, stop, delete where supported | Choose when to create or clean up rooms | | Feed distribution | Create a room post from an existing `roomId` where supported | Decide the target feed and product copy | | Broadcast credentials | Return broadcaster token or URL data where supported | Hand credentials to your media or LiveKit integration | | Live viewing | Expose room playback fields such as live playback URL and thumbnails | Build the player, controls, and viewer UX | | Room state | Expose statuses and realtime observers | React to state transitions in UI and operations | ## Room Workflow Create the room with a title and optional room configuration. Use [Create Room](./create-room) for platform-specific examples. Publish the room to a user or community feed with `createRoomPost()`. See [Room Posts](/social-plus-sdk/social/content-management/posts/creation/room-post). Request broadcaster data for the room. TypeScript and Android expose typed broadcaster-data APIs; iOS exposes room token generation. Connect your app's media stack, such as a LiveKit client, using the returned credentials. This connection is outside the social.plus SDK. Observe the room object or room collection for lifecycle changes, then call the stop API when the broadcast ends. ## Room Statuses | Status | Meaning | Platform notes | | --- | --- | --- | | `idle` | Room exists but is not currently live | Available across TypeScript, iOS, and Android. | | `live` | Room is actively broadcasting | Available across TypeScript, iOS, and Android. | | `waitingReconnect` | Room is waiting for the broadcaster to reconnect | Available across TypeScript, iOS, and Android. | | `ended` | Live session has ended | Available across TypeScript, iOS, and Android. | | `recorded` | Recorded playback metadata is available | Available across TypeScript, iOS, and Android. | | `terminated` | Room was terminated | Exposed by TypeScript and iOS room status types. | | `error` | Room entered an error state | Available across TypeScript, iOS, and Android. | ## Related Topics Review the room model, statuses, participants, and playback fields. Create a room and request broadcaster data with platform-specific SDK snippets. Query, observe, update, stop, and delete room records. Invite, remove, and observe co-host room participants. Use room playback fields to power viewer playback. Access recorded playback metadata after a room is recorded. --- ### [Rooms Overview](https://learn.social.plus/social-plus-sdk/video-new/broadcasting/rooms-overview) > SDK-backed model overview for live rooms, co-host rooms, playback fields, and room state. Rooms are SDK records for live broadcasting sessions. A room can be observed as a single live object or queried as a live collection, then published into a feed by creating a room post from its `roomId`. This page focuses on source-backed room concepts. Use [Create Room](./create-room) for runnable snippets. ## Platform Availability | Platform | Room model | Single room observer | Room list/query | Room post | | --- | --- | --- | --- | --- | | TypeScript | `Amity.Room` | `RoomRepository.getRoom(roomId, callback)` | `RoomRepository.getRooms(params, callback)` | `PostRepository.createRoomPost()` | | iOS | `AmityRoom` | `AmityRoomRepository().getRoom(withId:)` | `AmityRoomRepository().getRooms(with:)` | `AmityPostRepository().createRoomPost()` | | Android | `AmityRoom` | `AmityVideoClient.newRoomRepository().getRoom(roomId)` | `AmityVideoClient.newRoomRepository().getRooms().build().query()` | `AmitySocialClient.newPostRepository().createRoomPost()` | | Flutter | No current public `AmityRoom` model found | Not available | Not available | Not available | ## Room Types | Type value | Meaning | Platform notes | | --- | --- | --- | | `coHosts` | Co-host live room type | TypeScript and iOS accept this value during creation. Android exposes it as `AmityRoomType.CO_HOSTS` for queries, but current `createRoom()` does not expose a room type argument. | | `directStreaming` | Direct streaming room type | TypeScript and iOS expose this value. Android exposes it as `AmityRoomType.DIRECT_STREAMING` for queries. | ## Room Fields | Concept | TypeScript | iOS | Android | | --- | --- | --- | --- | | Room ID | `room.roomId` | `room.roomId` | `room.getRoomId()` | | Status | `room.status` | `room.status` | `room.getStatus()` | | Title and description | `title`, `description` | `title`, `description` | `getTitle()`, `getDescription()` | | Chat channel linkage | `liveChannelEnabled`, `liveChannelId`, `getLiveChat()` | `channelEnabled`, `channelId`, `channel` | `isChannelEnabled()`, `getChannelId()`, `getChannel()` | | Feed/post reference | `referenceType`, `referenceId`, `post` | `referenceType`, `referenceId`, `post` | `getReferenceType()`, `getReferenceId()`, `getPost()` | | Live playback | `livePlaybackUrl`, `liveThumbnailUrl`, `liveResolution` | `livePlaybackUrl`, `liveThumbnailUrl`, `liveResolution` | `getLivePlaybackUrl()`, `getLiveThumbnailUrl()`, `getLiveResolution()` | | Recorded playback | `recordedPlaybackInfos`, `recordedResolution` | `recordedData`, `recordedResolution` | `getRecordedPlaybackInfos()`, `getRecordedResolution()` | | Parent and child rooms | `parentRoomId`, `childRoomIds`, `childRooms` | `parentRoomId`, `childRoomIds`, `childRooms` | `getParentRoomId()`, `getChildRoomIds()`, `getChildRooms()` | | Creator and deletion | `createdBy`, `isDeleted`, `deletedBy` | `creatorId`, `creator`, `isDeleted`, `deletedById` | `getCreatorId()`, `getCreator()`, `isDeleted()` | | Custom metadata | `metadata` | `metadata` | `getMetadata()` | | Moderation | `moderation` | `moderation` | `getModeration()` | Android's current public `AmityRoom` model does not expose a direct `getType()` accessor even though room type is available in query filters and internal model construction. ## Participants Participants are users who can broadcast in the room. Viewers are not room participants. | Field | TypeScript | iOS | Android | | --- | --- | --- | --- | | Role/type | `type: "host" | "coHost"` | `type: String` | `ParticipantType.HOST`, `ParticipantType.CoHost`, or `ParticipantType.Unknown` | | User ID | `userId` | `userId` | `userId` | | Internal user ID | `userInternalId` | `userInternalId` | `userInternalId` | | Linked user | `user` | `user` | `user` | | Product-tag permission | `canManageProductTags` | `canManageProductTags` | `canManageProductTags` | ## Status Model | Status | Meaning | | --- | --- | | `idle` | The room exists but is not broadcasting. | | `live` | The room is currently broadcasting. | | `waitingReconnect` | The live session is waiting for reconnection. | | `ended` | The live session has stopped. | | `recorded` | Recorded playback metadata is available. | | `terminated` | The room was terminated. TypeScript and iOS expose this status. | | `error` | The room is in an error state. | ## Room Posts and Feed Distribution Creating a room does not by itself document a feed post in the SDK examples. To publish the room into a feed, create a room post with the room ID. Create a user or community feed post that references an existing room. Create a room before publishing it as a room post. ## Media and Playback Fields The room model exposes media fields that your app can use for live or recorded playback. The SDK does not render the player for SDK integrations. | Field | Availability | Use | | --- | --- | --- | | `livePlaybackUrl` | While live when available | Viewer playback URL. | | `liveThumbnailUrl` | Live or reconnecting sessions when available | Live thumbnail generated by the streaming backend. | | `liveResolution` | Live or reconnecting sessions when available | Live stream aspect ratio, width, and height. | | `recordedPlaybackInfos` / `recordedData` | After recording is available | Recorded playback URL and thumbnail metadata. | | `recordedResolution` | After recording is available | Recorded stream aspect ratio, width, and height. | ## Next Steps Create rooms and retrieve broadcaster data. Observe, query, update, stop, and delete rooms. Manage co-host invitations, removal, and permissions. Read recorded playback fields after processing completes. --- ### [Create Room](https://learn.social.plus/social-plus-sdk/video-new/broadcasting/create-room) > Create live rooms and retrieve broadcaster data with the current SDK room APIs. Create a room before you publish it to a feed or connect a broadcaster. Room creation stores the room record and initial configuration; creating the feed post and connecting the media layer are separate steps. Flutter does not currently expose a public room broadcasting repository in the audited SDK source. Use TypeScript, iOS, Android, or a backend-supported flow when your product needs to create rooms. ## Parameters | Parameter | Required | Platform support | Description | | --- | --- | --- | --- | | `title` | TypeScript and Android: yes. iOS initializer accepts `nil`; send a title for usable rooms. | TypeScript, iOS, Android | Room title shown in your product experience. | | `description` | No | TypeScript, iOS, Android | Optional room description. | | `thumbnailFileId` | No | TypeScript, iOS, Android | File ID for an uploaded thumbnail image. | | `metadata` | No | TypeScript, iOS, Android | Custom key-value data stored with the room. | | `liveChatEnabled` | No | TypeScript, iOS, Android | Whether the room should support live chat linkage. iOS defaults this to `true`. | | `parentRoomId` | No | TypeScript, iOS, Android | Parent room ID for room hierarchy scenarios. | | `participants` | No | iOS, Android | Initial participant user IDs. TypeScript room creation does not expose this field. | | `type` | No | TypeScript, iOS | Room type such as `coHosts` / `.coHosts` or `directStreaming` / `.directStreaming`. Android creation does not expose a type argument. | ## Create a Room Create a room with the title and configuration your product needs before publishing it or connecting a broadcaster. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; const { data: room } = await RoomRepository.createRoom({ title: "Product Launch Event", description: "Join us for the unveiling of our latest features", liveChatEnabled: true, type: "coHosts", metadata: { category: "education", }, }); showSuccessMessage(room.roomId); ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient import com.google.gson.JsonObject val metadata = JsonObject().apply { addProperty("category", "education") } AmityVideoClient.newRoomRepository() .createRoom( title = "Product Launch Event", description = "Join us for the unveiling of our latest features", metadata = metadata, liveChatEnabled = true, participants = listOf(userId) ) .subscribe( { room -> showSuccessMessage(room.getRoomId()) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS let options = AmityRoomCreateOptions( title: "Product Launch Event", description: "Join us for the unveiling of our latest features", metadata: ["category": "education"], liveChatEnabled: true, participants: [userId], type: .coHosts ) let room = try await AmityRoomRepository().createRoom(with: options) showSuccessMessage(room.roomId) ``` ## Create with Thumbnail or Parent Room Use `thumbnailFileId` after uploading an image through the file APIs. Use `parentRoomId` only when your product intentionally creates a room hierarchy. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; const { data: childRoom } = await RoomRepository.createRoom({ title: "Regional Breakout", thumbnailFileId: imageFileId, parentRoomId: roomId, liveChatEnabled: true, }); showSuccessMessage(childRoom.roomId); ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient AmityVideoClient.newRoomRepository() .createRoom( title = "Regional Breakout", thumbnailFileId = imageFileId, parentRoomId = roomId, liveChatEnabled = true ) .subscribe( { room -> showSuccessMessage(room.getRoomId()) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS let options = AmityRoomCreateOptions( title: "Regional Breakout", thumbnailFileId: imageFileId, liveChatEnabled: true, parentRoomId: roomId ) let childRoom = try await AmityRoomRepository().createRoom(with: options) showSuccessMessage(childRoom.roomId) ``` ## Get Broadcaster Data After creating the room, request broadcaster credentials before connecting the media layer. TypeScript and Android expose broadcaster-data APIs. iOS exposes room token generation. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; const broadcasterData = await RoomRepository.getBroadcasterData(roomId); if (broadcasterData.coHostUrl && broadcasterData.coHostToken) { showSuccessMessage(broadcasterData.coHostUrl); } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient import com.amity.socialcloud.sdk.model.video.room.AmityRoomBroadcastData AmityVideoClient.newRoomRepository() .getBroadcasterData(roomId) .subscribe( { broadcasterData -> when (broadcasterData) { is AmityRoomBroadcastData.CoHosts -> { showSuccessMessage(broadcasterData.getCoHostUrl()) } is AmityRoomBroadcastData.DirectStreaming -> { showSuccessMessage(broadcasterData.getDirectStreamUrl()) } } }, { error -> handleGeneralError(error) } ) ``` ```swift iOS let broadcasterData = try await AmityRoomRepository() .generateRoomToken(withId: roomId) let coHostUrl = broadcasterData?["coHostUrl"] as? String let coHostToken = broadcasterData?["coHostToken"] as? String if let coHostUrl, let coHostToken { showSuccessMessage("\(coHostUrl):\(coHostToken)") } ``` Use the returned broadcaster data with your media stack, such as a LiveKit client. The social.plus SDK returns room and credential data; it does not publish camera or microphone tracks for your SDK integration. ## Publish the Room To show the room in a user or community feed, create a room post from the room ID. Create a feed post that references an existing room. Review room fields, statuses, participants, and playback metadata. ## Platform Notes - TypeScript exposes `RoomRepository.createRoom()` and returns `Amity.Cached`. - iOS creates rooms with `AmityRoomCreateOptions` and `AmityRoomRepository().createRoom(with:)`. - Android creates rooms with `AmityVideoClient.newRoomRepository().createRoom(...)`. - Flutter does not currently expose a public room creation API in the audited SDK source. ## Related Topics Observe, query, update, stop, and delete rooms after creation. Connect the broadcaster after retrieving credentials. --- ### [Start Broadcasting](https://learn.social.plus/social-plus-sdk/video-new/broadcasting/start-broadcasting) > Fetch room broadcaster credentials, hand them to your media stack, observe lifecycle changes, and stop the room. Starting a broadcast has two parts: the social.plus SDK returns room broadcaster credentials, and your app-owned media stack uses those credentials to connect and publish audio/video. Keep those responsibilities separate in your implementation and docs. This page covers SDK room broadcasting APIs. Camera, microphone, LiveKit client setup, permissions, and media publishing UI are app-owned concerns after the SDK returns broadcaster data. ## Platform Surface | Platform | Get broadcaster data | Observe lifecycle | Stop broadcast | Notes | | --- | --- | --- | --- | --- | | TypeScript | `RoomRepository.getBroadcasterData(roomId)` | `RoomRepository.onRoomStartBroadcasting(...)`, `onRoomEndBroadcasting(...)`, or `getRoom(...)` | `RoomRepository.stopRoom(roomId)` | `Amity.BroadcasterData` can include `coHostToken`, `coHostUrl`, and `directStreamUrl`. | | iOS | `AmityRoomRepository().generateRoomToken(withId:)` | `AmityRoomRepository().getRoom(withId:)` live object | `AmityRoomRepository().stopRoom(withId:)` | Token response is a dictionary from `/api/v1/rooms/{roomId}/token`; read known keys defensively. | | Android | `AmityVideoClient.newRoomRepository().getBroadcasterData(roomId)` | `getRoom(roomId)` plus room topic subscription when real-time updates are needed | `stopRoom(roomId)` | Returns `AmityRoomBroadcastData.CoHosts` or `AmityRoomBroadcastData.DirectStreaming`. | | Flutter | No current public room broadcaster API found in this audit | Not available | Not available | The Flutter SDK source exposes older stream read APIs, not the room broadcasting repository. | ## Parameters | Concept | Platforms | TypeScript | iOS | Android | Notes | | --- | --- | --- | --- | --- | --- | | Fetch credentials | TypeScript, iOS, Android | `getBroadcasterData(roomId)` | `generateRoomToken(withId:)` | `getBroadcasterData(roomId)` | Requires an existing room ID. | | Co-host URL | TypeScript, iOS, Android | `coHostUrl?: string` | `tokenPayload["coHostUrl"]` | `AmityRoomBroadcastData.CoHosts.getCoHostUrl()` | Use as the media connection URL for co-host rooms. | | Co-host token | TypeScript, iOS, Android | `coHostToken?: string` | `tokenPayload["coHostToken"]` | `AmityRoomBroadcastData.CoHosts.getCoHostToken()` | Use as the media access token for co-host rooms. | | Direct stream URL | TypeScript, iOS, Android | `directStreamUrl?: string` | `tokenPayload["directStreamUrl"]` | `AmityRoomBroadcastData.DirectStreaming.getDirectStreamUrl()` | Use only for direct-streaming publisher flows. | | Flutter room broadcasting | Flutter | Not applicable | Not applicable | Not applicable | No current public Flutter room broadcaster API found in this audit. | ### Lifecycle Parameters | Concept | Platforms | TypeScript | iOS | Android | | --- | --- | --- | --- | --- | | Start event | TypeScript, iOS, Android | `onRoomStartBroadcasting(callback)` | Observe `AmityRoom.status` via `getRoom(withId:)` | Observe `getRoom(roomId)` and subscribe to `AmityRoomEvents.STREAMER` when needed | | End event | TypeScript, iOS, Android | `onRoomEndBroadcasting(callback)` | Observe `AmityRoom.status` via `getRoom(withId:)` | Observe `getRoom(roomId)` and subscribe to `AmityRoomEvents.STREAMER` when needed | | Stop room | TypeScript, iOS, Android | `stopRoom(roomId)` | `stopRoom(withId:)` | `stopRoom(roomId)` | | Cleanup | TypeScript, iOS, Android | Call returned `Amity.Unsubscriber` functions | Retain and release the `AmityNotificationToken` with the screen lifecycle | Dispose Rx subscriptions and unsubscribe room topics when no longer needed | ## Get Broadcaster Data Call this after the room exists and before connecting your app-owned media client. Co-host rooms use `coHostUrl` and `coHostToken`; direct-streaming rooms use `directStreamUrl`. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; async function getBroadcastCredentials(roomId: string) { const credentials = await RoomRepository.getBroadcasterData(roomId); if (credentials.coHostUrl && credentials.coHostToken) { return { mode: "coHosts" as const, url: credentials.coHostUrl, token: credentials.coHostToken, }; } if (credentials.directStreamUrl) { return { mode: "directStreaming" as const, directStreamUrl: credentials.directStreamUrl, }; } throw new Error("No broadcaster credentials returned for this room."); } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient import com.amity.socialcloud.sdk.model.video.room.AmityRoomBroadcastData val disposable = AmityVideoClient.newRoomRepository() .getBroadcasterData(roomId) .subscribe( { broadcastData -> when (broadcastData) { is AmityRoomBroadcastData.CoHosts -> { showSuccessMessage(broadcastData.getCoHostUrl()) showSuccessMessage(broadcastData.getCoHostToken()) } is AmityRoomBroadcastData.DirectStreaming -> { showSuccessMessage(broadcastData.getDirectStreamUrl()) } } }, { error -> handleGeneralError(error) } ) ``` ```swift iOS let tokenPayload = try await AmityRoomRepository() .generateRoomToken(withId: roomId) if let coHostUrl = tokenPayload?["coHostUrl"] as? String, let coHostToken = tokenPayload?["coHostToken"] as? String { showSuccessMessage(coHostUrl) showSuccessMessage(coHostToken) } else if let directStreamUrl = tokenPayload?["directStreamUrl"] as? String { showSuccessMessage(directStreamUrl) } ``` ## Hand Credentials to Your Media Client The SDK does not publish camera or microphone tracks. Use the returned URL/token with your media client, then let that client own connection, preview, mute, retry, and device-selection behavior. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; type ExternalMediaClient = { connect: (url: string, token: string) => Promise; publishCamera: () => Promise; }; async function startCoHostBroadcast( roomId: string, mediaClient: ExternalMediaClient, ) { const data = await RoomRepository.getBroadcasterData(roomId); if (!data.coHostUrl || !data.coHostToken) { throw new Error("This room did not return co-host broadcaster credentials."); } await mediaClient.connect(data.coHostUrl, data.coHostToken); await mediaClient.publishCamera(); } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient import com.amity.socialcloud.sdk.model.video.room.AmityRoomBroadcastData import io.reactivex.rxjava3.core.Completable fun connectExternalMedia(url: String, token: String): Completable { showSuccessMessage(url) showSuccessMessage(token) return Completable.complete() } fun publishExternalCamera(): Completable = Completable.complete() val disposable = AmityVideoClient.newRoomRepository() .getBroadcasterData(roomId) .flatMapCompletable { broadcastData -> when (broadcastData) { is AmityRoomBroadcastData.CoHosts -> { connectExternalMedia( url = broadcastData.getCoHostUrl(), token = broadcastData.getCoHostToken() ).andThen(publishExternalCamera()) } is AmityRoomBroadcastData.DirectStreaming -> { Completable.error( IllegalStateException("Use directStreamUrl with your RTMP publisher.") ) } } } .subscribe( { showSuccessMessage(roomId) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS func connectExternalMedia(url: String, token: String) async throws { showSuccessMessage(url) showSuccessMessage(token) } func publishExternalCamera() async throws { showSuccessMessage("camera") } let tokenPayload = try await AmityRoomRepository() .generateRoomToken(withId: roomId) guard let coHostUrl = tokenPayload?["coHostUrl"] as? String, let coHostToken = tokenPayload?["coHostToken"] as? String else { throw NSError(domain: "Broadcast", code: 0) } try await connectExternalMedia(url: coHostUrl, token: coHostToken) try await publishExternalCamera() ``` ## Observe Broadcast Lifecycle Use lifecycle updates to keep host UI, viewer entry points, and moderation tools aligned with the room status. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; function observeBroadcastLifecycle(roomId: string): Amity.Unsubscriber { const stopStarted = RoomRepository.onRoomStartBroadcasting(room => { if (room.roomId === roomId) { showSuccessMessage(room.status); } }); const stopEnded = RoomRepository.onRoomEndBroadcasting(room => { if (room.roomId === roomId) { showSuccessMessage(room.status); } }); return () => { stopStarted(); stopEnded(); }; } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient import com.amity.socialcloud.sdk.model.core.events.AmityRoomEvents val roomRepository = AmityVideoClient.newRoomRepository() val roomDisposable = roomRepository .getRoom(roomId) .subscribe( { room -> showSuccessMessage(room.getStatus()) }, { error -> handleGeneralError(error) } ) val topicDisposable = roomRepository .getRoom(roomId) .firstOrError() .flatMapCompletable { room -> room.subscription(AmityRoomEvents.STREAMER).subscribeTopic() } .subscribe( { showSuccessMessage(roomId) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS var roomObservationToken: AmityNotificationToken? let roomObject = AmityRoomRepository().getRoom(withId: roomId) roomObservationToken = roomObject.observe { liveObject, error in if let error { handleGeneralError(error) return } if let room = liveObject.snapshot { showSuccessMessage(room.status.rawValue) } } showSuccessMessage(roomObservationToken != nil) ``` ## Stop the Broadcast Stop the room when the host ends the session. Disconnect your media client separately, then call the SDK stop API so social.plus room state and viewer surfaces can move out of the live state. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; async function stopBroadcast(roomId: string) { const { data: stoppedRoom } = await RoomRepository.stopRoom(roomId); showSuccessMessage(stoppedRoom.status); } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient val disposable = AmityVideoClient.newRoomRepository() .stopRoom(roomId) .subscribe( { showSuccessMessage(roomId) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS let stoppedRoom = try await AmityRoomRepository() .stopRoom(withId: roomId) showSuccessMessage(stoppedRoom.status.rawValue) ``` Stopping a room ends the current broadcast session. Do not build a "restart the same room" flow unless your product and backend contract explicitly support it. ## Media Boundary | Area | Owned by social.plus SDK | Owned by your app/media stack | | --- | --- | --- | | Room record | Create, observe, update, stop, delete | Product routing and host controls | | Credentials | Return room broadcaster fields | Store only in memory and hand to the media client | | Publishing | Not handled by social.plus SDK room APIs | Camera, microphone, preview, mute, reconnect, and permissions | | Viewer state | Room status, live playback fields, recorded metadata | Player UI and playback SDK behavior | ## Related Topics Create the room before fetching broadcaster credentials. Invite, observe, and manage co-hosts before or during the broadcast. Show viewers how to watch active room broadcasts. --- ### [Live Room Viewing](https://learn.social.plus/social-plus-sdk/video-new/broadcasting/live-viewing) > Discover room posts, observe room playback state, and hand SDK playback URLs to your player. Live room viewing starts from a room ID or a room post. The social.plus SDK owns room discovery, room status, live playback URLs, recorded playback metadata, and live room post collections. Your app owns the actual video player, buffering UI, autoplay policy, and platform playback SDK. This page covers SDK room viewing data. AVPlayer, ExoPlayer, HLS.js, browser autoplay handling, DRM, and player UI are app-owned concerns after the SDK returns a playback URL. ## Platform Surface | Platform | Find room posts | Observe room | Live playback field | Recorded playback field | | --- | --- | --- | --- | --- | | TypeScript | `PostRepository.getLiveRoomPosts(...)`, `getCommunityLiveRoomPosts(...)` | `RoomRepository.getRoom(roomId, callback)` | `room.livePlaybackUrl` | `room.recordedPlaybackInfos[]` | | iOS | `AmityPostRepository().getLiveRoomPosts()`, `getCommunityLiveRoomPosts(withIds:)` | `AmityRoomRepository().getRoom(withId:)` | `room.livePlaybackUrl` | `room.recordedData[]` | | Android | `AmityPostRepository.getLiveRoomPosts()`, `getCommunityLiveRoomPosts(...)` | `AmityVideoClient.newRoomRepository().getRoom(roomId)` | `room.getLivePlaybackUrl()` | `room.getRecordedPlaybackInfos()` | | Flutter | No current public room viewing API found in this audit | Not available | Not available | Not available | ## Parameters | Concept | Platforms | TypeScript | iOS | Android | | --- | --- | --- | --- | --- | | Live room post collection | TypeScript, iOS, Android | `getLiveRoomPosts(callback)` | `getLiveRoomPosts()` | `getLiveRoomPosts()` | | Community live room posts | TypeScript, iOS, Android | `getCommunityLiveRoomPosts({ communityIds })` | `getCommunityLiveRoomPosts(withIds:)` | `getCommunityLiveRoomPosts(communityIds)` | | Room ID in post data | TypeScript, iOS, Android | `post.data.roomId` when `dataType === "room"` | `post.data?["roomId"]` when `dataType == "room"` | `AmityPost.Data.ROOM.getRoomId()` | | Observe room | TypeScript, iOS, Android | `RoomRepository.getRoom(roomId, callback)` | `AmityRoomRepository().getRoom(withId:)` | `AmityVideoClient.newRoomRepository().getRoom(roomId)` | | Live URL | TypeScript, iOS, Android | `room.livePlaybackUrl` | `room.livePlaybackUrl` | `room.getLivePlaybackUrl()` | | Recorded URL | TypeScript, iOS, Android | `room.recordedPlaybackInfos[].url` | `room.recordedData[].playbackUrl` | `room.getRecordedPlaybackInfos()[].url` | ## Find Live Room Posts Use live room post collections when your product shows a live shelf or a community-specific live section. Extract the `roomId` from room post data, then observe the room itself for playback state. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; function observeCommunityLiveRoomPosts(communityId: string): Amity.Unsubscriber { return PostRepository.getCommunityLiveRoomPosts( { communityIds: [communityId] }, snapshot => { if (snapshot.error) { handleError(snapshot.error); return; } snapshot.data.forEach(post => { const roomData = post.data as { roomId?: string } | undefined; if (post.dataType === "room" && roomData?.roomId) { showSuccessMessage(roomData.roomId); } }); }, ); } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.social.AmitySocialClient import com.amity.socialcloud.sdk.model.social.post.AmityPost val disposable = AmitySocialClient.newPostRepository() .getCommunityLiveRoomPosts(listOf(communityId)) .subscribe( { posts -> posts.forEach { post -> val roomData = post.getData() if (roomData is AmityPost.Data.ROOM) { showSuccessMessage(roomData.getRoomId()) } } }, { error -> handleGeneralError(error) } ) ``` ```swift iOS var liveRoomPostsToken: AmityNotificationToken? let liveRoomPosts = AmityPostRepository() .getCommunityLiveRoomPosts(withIds: [communityId]) liveRoomPostsToken = liveRoomPosts.observe { collection, error in if let error { handleGeneralError(error) return } collection.snapshots.forEach { post in if post.dataType == "room", let roomId = post.data?["roomId"] as? String { showSuccessMessage(roomId) } } } ``` ## Observe Room Playback State Observe the room record while the viewing screen is open. A live object update can move the UI from waiting, to live playback, to ended, and later to recorded playback. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; function observeRoomPlayback(roomId: string): Amity.Unsubscriber { return RoomRepository.getRoom(roomId, snapshot => { if (snapshot.error) { handleError(snapshot.error); return; } const room = snapshot.data; if (room.status === "live" && room.livePlaybackUrl) { showSuccessMessage(room.livePlaybackUrl); return; } if (room.status === "recorded") { showSuccessMessage(room.recordedPlaybackInfos[0]?.url); return; } showSuccessMessage(room.status); }); } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient import com.amity.socialcloud.sdk.model.video.room.AmityRoomStatus val disposable = AmityVideoClient.newRoomRepository() .getRoom(roomId) .subscribe( { room -> when (room.getStatus()) { AmityRoomStatus.LIVE -> { room.getLivePlaybackUrl()?.let { showSuccessMessage(it) } } AmityRoomStatus.RECORDED -> { room.getRecordedPlaybackInfos() .firstOrNull() ?.url ?.let { showSuccessMessage(it) } } else -> { showSuccessMessage(room.getStatus()) } } }, { error -> handleGeneralError(error) } ) ``` ```swift iOS var roomObservationToken: AmityNotificationToken? let roomObject = AmityRoomRepository().getRoom(withId: roomId) roomObservationToken = roomObject.observe { liveObject, error in if let error { handleGeneralError(error) return } guard let room = liveObject.snapshot else { return } switch room.status { case .live, .waitingReconnect: showSuccessMessage(room.livePlaybackUrl) case .recorded: showSuccessMessage(room.recordedData.first?.playbackUrl) default: showSuccessMessage(room.status.rawValue) } } ``` ## Choose a Playback Source Keep playback source selection small and deterministic. Pass the selected URL to your own player layer only when the SDK room state has a playable source. ```typescript TypeScript function playbackSourceForRoom(room: Amity.Room): string | undefined { if (room.status === "live" || room.status === "waitingReconnect") { return room.livePlaybackUrl; } if (room.status === "recorded") { return room.recordedPlaybackInfos[0]?.url; } return undefined; } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.video.room.AmityRoom import com.amity.socialcloud.sdk.model.video.room.AmityRoomStatus fun playbackSourceForRoom(room: AmityRoom): String? { return when (room.getStatus()) { AmityRoomStatus.LIVE, AmityRoomStatus.WAITING_RECONNECT -> room.getLivePlaybackUrl() AmityRoomStatus.RECORDED -> room.getRecordedPlaybackInfos() .firstOrNull() ?.url else -> null } } ``` ```swift iOS func playbackSource(for room: AmityRoom) -> String? { switch room.status { case .live, .waitingReconnect: return room.livePlaybackUrl case .recorded: return room.recordedData.first?.playbackUrl default: return nil } } ``` ## Status Handling | Status | Viewer behavior | | --- | --- | | `idle` | Show a waiting or scheduled state. | | `live` | Use the live playback URL when present. | | `waitingReconnect` | Keep the player UI available, but show reconnecting state if playback stalls. | | `ended` | Stop live playback and show a processing state while recorded playback is not ready. | | `recorded` | Use recorded playback metadata. | | `error` | Show a recoverable error state and let the user retry or leave. | If a room is live but the SDK returns no live playback URL, do not invent a fallback URL. Treat it as unavailable for the current viewer and show a product-specific blocked, unavailable, or retry state. ## Player Boundary | Area | Owned by social.plus SDK | Owned by your app/player | | --- | --- | --- | | Discovery | Live room post collections and room IDs | Placement, ranking, and empty states | | State | Room status and live object updates | Player state machine and user-facing copy | | Playback source | `livePlaybackUrl` and recorded playback metadata | AVPlayer, ExoPlayer, HLS.js, web video, buffering, and errors | | Cleanup | Observer unsubscribe, token release, Rx disposal | Player teardown and audio/video session cleanup | ## Related Topics Fetch broadcaster credentials and start the host-side media session. Observe, stop, update, or delete room records. Handle recorded playback after a live room ends. --- ### [Manage Rooms](https://learn.social.plus/social-plus-sdk/video-new/broadcasting/manage-rooms) > Observe, query, update, stop, and delete live rooms with current SDK room APIs. Manage rooms after creation by observing a single room, querying room lists, updating room metadata, stopping a live session, or deleting a room record. This page covers SDK room management APIs. Media publishing, LiveKit connection handling, and player UI are app-owned concerns. ## Platform Surface | Platform | Single room | Room list | Mutations | Notes | | --- | --- | --- | --- | --- | | TypeScript | `RoomRepository.getRoom(roomId, callback)` | `RoomRepository.getRooms(params, callback)` | `updateRoom()`, `stopRoom()`, `deleteRoom()` | Live callbacks return an unsubscribe function. Room list pagination uses `hasNextPage` and `onNextPage`. | | iOS | `AmityRoomRepository().getRoom(withId:)` | `AmityRoomRepository().getRooms(with:)` | `updateRoom(withId:options:)`, `stopRoom(withId:)`, `deleteRoom(withId:)` | Retain the returned `AmityNotificationToken` while observing. | | Android | `AmityVideoClient.newRoomRepository().getRoom(roomId)` | `AmityVideoClient.newRoomRepository().getRooms().build().query()` | `updateRoom()`, `stopRoom()`, `deleteRoom()` | Single rooms are `Flowable`; room lists are `Flowable>`. | | Flutter | No current public room repository found in this audit | Not available | Not available | The Flutter SDK source exposes older stream APIs, not the room broadcasting repository. | ## Parameters | Concept | TypeScript | iOS | Android | | --- | --- | --- | --- | | Status filter | `statuses?: Amity.RoomStatus[]` | `statuses: [AmityRoomStatus]?` | `setStatuses(Array)` | | Room type filter | `type?: Amity.RoomType` | `type: AmityRoomType?` | `setTypes(Array)` | | Deleted rooms | `includeDeleted?: boolean` | `isDeleted: Bool` | `setIsDeleted(Boolean?)` | | Sort order | `sortBy?: "firstCreated"` or `"lastCreated"` | `sortBy: AmityRoomSortOption` | `setSortBy(AmityRoomSortOption?)` | | Page size | `limit?: number` | Not exposed on `AmityRoomQueryOptions` | Paging 3 controls consumption after `PagingData` emission | ### Mutation Parameters | Operation | TypeScript | iOS | Android | | --- | --- | --- | --- | | Update | `updateRoom(roomId, bundle)` | `updateRoom(withId:options:)` | `updateRoom(roomId, ...)` | | Stop | `stopRoom(roomId)` | `stopRoom(withId:)` | `stopRoom(roomId)` | | Delete | `deleteRoom(roomId)` | `deleteRoom(withId:)` | `deleteRoom(roomId)` | | Update field | TypeScript | iOS | Android | | --- | --- | --- | --- | | Title | `title` | `title` | `title` | | Description | `description` | `description` | `description` | | Thumbnail | `thumbnailFileId` | `thumbnailFileId` | `thumbnailFileId` | | Metadata | `metadata` | `metadata` | `metadata` | | Live chat flag | `liveChatEnabled` | `channelEnabled` | `liveChatEnabled` | | Parent room | `parentRoomId` | Not exposed by update options | Not exposed by update API | ## Get a Room Use the single-room API when a screen needs live updates for one room. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; function observeRoom(roomId: string): Amity.Unsubscriber { return RoomRepository.getRoom(roomId, snapshot => { if (snapshot.error) { handleError(snapshot.error); return; } showSuccessMessage(snapshot.data.status); }); } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient val disposable = AmityVideoClient.newRoomRepository() .getRoom(roomId) .subscribe( { room -> showSuccessMessage(room.getStatus()) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS var roomObservationToken: AmityNotificationToken? let roomObject = AmityRoomRepository().getRoom(withId: roomId) roomObservationToken = roomObject.observe { liveObject, error in if let error { handleGeneralError(error) return } if let room = liveObject.snapshot { showSuccessMessage(room.status.rawValue) } } ``` ## Query Rooms Use the room-list API for discovery, dashboards, moderation queues, or live-room shelves. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; function observeLiveRooms(): Amity.Unsubscriber { return RoomRepository.getRooms( { statuses: ["live"], type: "coHosts", sortBy: "lastCreated", includeDeleted: false, limit: 20, }, snapshot => { if (snapshot.error) { handleError(snapshot.error); return; } renderResults(snapshot.data); if (snapshot.hasNextPage) { snapshot.onNextPage?.(); } }, ); } ``` ```kotlin Android import androidx.paging.PagingData import com.amity.socialcloud.sdk.api.video.AmityVideoClient import com.amity.socialcloud.sdk.model.video.room.AmityRoom import com.amity.socialcloud.sdk.model.video.room.AmityRoomSortOption import com.amity.socialcloud.sdk.model.video.room.AmityRoomStatus import com.amity.socialcloud.sdk.model.video.room.AmityRoomType val disposable = AmityVideoClient.newRoomRepository() .getRooms() .setStatuses(arrayOf(AmityRoomStatus.LIVE)) .setTypes(arrayOf(AmityRoomType.CO_HOSTS)) .setIsDeleted(false) .setSortBy(AmityRoomSortOption.LastCreated) .build() .query() .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS var roomCollectionToken: AmityNotificationToken? let options = AmityRoomQueryOptions( statuses: [.live], type: .coHosts, isDeleted: false, sortBy: .lastCreated ) let rooms = AmityRoomRepository().getRooms(with: options) roomCollectionToken = rooms.observe { collection, error in if let error { handleGeneralError(error) return } showSuccessMessage(collection.snapshots.map { $0.roomId }) if collection.hasNext { collection.nextPage() } } ``` TypeScript filters by a single `type` value. Android's builder accepts `setTypes(...)` because the Android query model supports an array of room types. ## Update a Room Update room display fields and metadata. Participants, room type, and room identity are not update fields in these APIs. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; const { data: updatedRoom } = await RoomRepository.updateRoom(roomId, { title: "Updated Room Title", description: "New description", thumbnailFileId: imageFileId, metadata: { category: "updated", }, liveChatEnabled: true, }); showSuccessMessage(updatedRoom.roomId); ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient import com.google.gson.JsonObject val metadata = JsonObject().apply { addProperty("category", "updated") } AmityVideoClient.newRoomRepository() .updateRoom( roomId = roomId, title = "Updated Room Title", description = "New description", thumbnailFileId = imageFileId, metadata = metadata, liveChatEnabled = true ) .subscribe( { room -> showSuccessMessage(room.getRoomId()) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS let options = AmityRoomUpdateOptions( title: "Updated Room Title", description: "New description", thumbnailFileId: imageFileId, metadata: ["category": "updated"], channelEnabled: true ) let room = try await AmityRoomRepository() .updateRoom(withId: roomId, options: options) showSuccessMessage(room.roomId) ``` ## Stop or Delete a Room Stop a live session when broadcasting ends. Delete a room when your product flow should remove the room record. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; const { data: stoppedRoom } = await RoomRepository.stopRoom(roomId); showSuccessMessage(stoppedRoom.status); await RoomRepository.deleteRoom(roomId); showSuccessMessage(roomId); ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient val roomRepository = AmityVideoClient.newRoomRepository() roomRepository.stopRoom(roomId) .andThen(roomRepository.deleteRoom(roomId)) .subscribe( { showSuccessMessage(roomId) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS let roomRepository = AmityRoomRepository() let stoppedRoom = try await roomRepository.stopRoom(withId: roomId) showSuccessMessage(stoppedRoom.status.rawValue) try await roomRepository.deleteRoom(withId: roomId) showSuccessMessage(roomId) ``` Stopping a room ends the current broadcast session. Do not document or build a "restart same room" flow unless your product and backend contract explicitly support it. ## Recorded Playback Boundary Recorded playback is intentionally separate from room lifecycle management: - TypeScript exposes `RoomRepository.getRecordedUrl(roomId)`. - Android exposes `getRecordedUrls(roomId)`. - iOS reads recorded playback data from `AmityRoom.recordedData`. See [Recorded Playback](./recorded-playback) for playback-specific guidance. ## Related Topics Create a room before observing or mutating it. Review room fields, statuses, participants, and playback metadata. Manage co-host invitations, removal, and permissions. --- ### [Co-Host Management](https://learn.social.plus/social-plus-sdk/video-new/broadcasting/co-host-management) > Invite, observe, and manage room co-hosts with current SDK room and invitation APIs. Co-host management is built on room invitations, room participant events, and room participant mutations. Use these APIs when a room host invites another user to broadcast, when an invited user accepts or rejects the invitation, and when the host manages a co-host during the room. This page covers SDK co-host control APIs. LiveKit connection, camera and microphone publishing, and broadcaster UI are app-owned concerns after the SDK returns broadcaster data. ## Platform Surface | Platform | Invite | Respond | Events | Participant control | | --- | --- | --- | --- | --- | | TypeScript | `room.createInvitation(userId)` | `room.getInvitations()`, `invitation.accept()`, `invitation.reject()`, `InvitationRepository.cancelInvitation(invitationId)` | `InvitationRepository.getInvitations(...)`, `RoomRepository.onRoomParticipantJoined(...)`, `onRoomParticipantRemoved(...)`, and related room events | `RoomRepository.updateCohostPermission(...)`, `removeParticipant(...)`, `leaveRoom(...)` | | iOS | `AmityRoom.createInvitation(_:)` | `room.getInvitation()`, `invitation.accept()`, `invitation.reject()`, `room.cancelInvitation(_:)` | `AmityRoomRepository().getCoHostEvent(roomId:)` | `updateCohostPermissions(...)`, `removeParticipant(withId:userId:)`, `leaveRoom(withId:)` | | Android | `AmityRoom.createInvitation(userId)` | `room.getInvitation()`, `invitation.accept()`, `invitation.reject()`, `invitation.cancel()` | `AmityVideoClient.newRoomRepository().getCoHostEvent(roomId)` | `updateCohostPermission(...)`, `removeRoomParticipant(...)`, `leaveRoom(...)` | | Flutter | No current public room repository found in this audit | Not available | Not available | Not available | ## Parameters | Concept | TypeScript | iOS | Android | | --- | --- | --- | --- | | Invite one user | `room.createInvitation(userId)` | `room.createInvitation(userId)` | `room.createInvitation(userId)` | | Current user's pending room invitation | `room.getInvitations()` | `room.getInvitation()` | `room.getInvitation()` | | Accept | `invitation.accept()` | `invitation.accept()` | `invitation.accept()` | | Reject | `invitation.reject()` | `invitation.reject()` | `invitation.reject()` | | Cancel | `InvitationRepository.cancelInvitation(invitationId)` | `room.cancelInvitation(invitationId)` | `invitation.cancel()` | | Invitation type value | `livestreamCohostInvite` | `.livestreamCoHostInvite` | `AmityInvitationType.LIVESTREAM_COHOST` | | Pending status | `"pending"` | `.pending` | `AmityInvitationStatus.PENDING` | ### Event and Participant Control | Concept | TypeScript | iOS | Android | | --- | --- | --- | --- | | Room invitation events | `InvitationRepository.getInvitations({ targetId, targetType: "room" }, callback)` | `AmityInvitationRepository().getInvitations(targetId:targetType:)` or `AmityRoomRepository().getCoHostEvent(roomId:)` | `AmityCoreClient.newInvitationRepository().getInvitations(targetId, AmityInvitation.TargetType.ROOM.value)` or `getCoHostEvent(roomId)` | | Co-host joined | `RoomRepository.onRoomParticipantJoined(callback)` | `AmityCoHostEventType.coHostJoined` | `AmityCoHostEvent.CoHostJoined` | | Co-host left | `RoomRepository.onRoomParticipantLeft(callback)` | `AmityCoHostEventType.coHostLeft` | `AmityCoHostEvent.CoHostLeft` | | Co-host removed | `RoomRepository.onRoomParticipantRemoved(callback)` | `AmityCoHostEventType.coHostRemoved` | `AmityCoHostEvent.CoHostRemoved` | | Stage left | `RoomRepository.onRoomParticipantStageLeft(callback)` | `AmityCoHostEventType.coHostStageLeft` | No separate public sealed event in audited model | | Product-tag permission | `updateCohostPermission(roomId, cohostId, canManageProductTags)` | `updateCohostPermissions(roomId:cohostId:canManageProductTags:)` | `updateCohostPermission(roomId, cohostId, canManageProductTags)` | | Remove participant | `removeParticipant(roomId, participantUserId)` | `removeParticipant(withId:userId:)` | `removeRoomParticipant(roomId, userId)` | | Leave room | `leaveRoom(roomId)` | `leaveRoom(withId:)` | `leaveRoom(roomId)` | ## Invite a Co-Host Invite one user at a time. The invitation type is the room co-host invitation type under the hood. ```typescript TypeScript async function inviteCoHost(room: Amity.Room, cohostUserId: string) { await room.createInvitation(cohostUserId); showSuccessMessage(cohostUserId); } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.video.room.AmityRoom fun inviteCoHost(room: AmityRoom, cohostUserId: String) { room.createInvitation(cohostUserId) .subscribe( { showSuccessMessage(cohostUserId) }, { error -> handleGeneralError(error) } ) } ``` ```swift iOS func inviteCoHost(room: AmityRoom, cohostUserId: String) async throws { try await room.createInvitation(cohostUserId) showSuccessMessage(cohostUserId) } ``` ## Respond or Cancel Invited users accept or reject the pending invitation. Hosts cancel an invitation before it is accepted. ```typescript TypeScript import { InvitationRepository } from "@amityco/ts-sdk"; async function acceptPendingRoomInvitation(room: Amity.Room) { const invitation = await room.getInvitations(); if (invitation?.status === "pending") { await invitation.accept(); showSuccessMessage(invitation.invitationId); } } async function cancelRoomInvitation(invitationId: string) { await InvitationRepository.cancelInvitation(invitationId); showSuccessMessage(invitationId); } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.core.invitation.AmityInvitation import com.amity.socialcloud.sdk.model.video.room.AmityRoom import io.reactivex.rxjava3.core.Completable fun acceptPendingRoomInvitation(room: AmityRoom) { room.getInvitation() .flatMapCompletable { invitations -> invitations.firstOrNull()?.accept() ?: Completable.complete() } .subscribe( { showSuccessMessage(room.getRoomId()) }, { error -> handleGeneralError(error) } ) } fun cancelRoomInvitation(invitation: AmityInvitation) { invitation.cancel() .subscribe( { showSuccessMessage(invitation.getInvitationId()) }, { error -> handleGeneralError(error) } ) } ``` ```swift iOS func acceptPendingRoomInvitation(room: AmityRoom) async throws { guard let invitation = await room.getInvitation() else { return } if invitation.status == .pending { try await invitation.accept() showSuccessMessage(invitation.invitationId) } } func cancelRoomInvitation(room: AmityRoom, invitationId: String) async throws { try await room.cancelInvitation(invitationId) showSuccessMessage(invitationId) } ``` ## Observe Co-Host Events Use invitation events for invite status and participant events for active room membership changes. Keep the returned unsubscribe, disposable, or cancellable for as long as the screen needs updates. ```typescript TypeScript import { InvitationRepository, RoomRepository } from "@amityco/ts-sdk"; function observeCoHostEvents(roomId: string): Amity.Unsubscriber { const stopInvitationEvents = InvitationRepository.getInvitations( { targetId: roomId, targetType: "room" }, invitations => { invitations.forEach(invitation => { showSuccessMessage(invitation.status); }); }, ); const stopJoinedEvents = RoomRepository.onRoomParticipantJoined(event => { if (event.room.roomId === roomId) { showSuccessMessage(event.actorInternalId); } }); const stopRemovedEvents = RoomRepository.onRoomParticipantRemoved(event => { if (event.room.roomId === roomId) { showSuccessMessage(event.actorInternalId); } }); return () => { stopInvitationEvents(); stopJoinedEvents(); stopRemovedEvents(); }; } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient import com.amity.socialcloud.sdk.core.session.model.AmityCoHostEvent val disposable = AmityVideoClient.newRoomRepository() .getCoHostEvent(roomId) .subscribe( { event -> when (event) { is AmityCoHostEvent.CoHostInvited -> { showSuccessMessage(event.invitation.getStatus()) } is AmityCoHostEvent.CoHostJoined -> { showSuccessMessage(event.actorInternalId ?: "") } is AmityCoHostEvent.CoHostRemoved -> { showSuccessMessage(event.actorInternalId ?: "") } else -> { showSuccessMessage(event.roomId) } } }, { error -> handleGeneralError(error) } ) ``` ```swift iOS let roomRepository = AmityRoomRepository() var coHostEventCancellable: AnyCancellable? coHostEventCancellable = roomRepository .getCoHostEvent(roomId: roomId) .sink { event in switch event.type { case .invitationInvited, .invitationAccepted, .invitationRejected, .invitationCancelled: showSuccessMessage(event.invitation?.status.rawValue) case .coHostJoined, .coHostLeft, .coHostRemoved, .coHostStageLeft: showSuccessMessage(event.actorInternalId) default: showSuccessMessage(event.room.roomId) } } showSuccessMessage(coHostEventCancellable != nil) ``` ## Manage Active Co-Hosts Hosts can update whether a co-host can manage product tags, remove a co-host from the room, and co-hosts can leave the room. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; async function updateCoHostProductPermission( roomId: string, cohostUserId: string, ) { const { data: updatedRoom } = await RoomRepository.updateCohostPermission( roomId, cohostUserId, true, ); showSuccessMessage( updatedRoom.participants.find(participant => participant.userId === cohostUserId) ?.canManageProductTags, ); } async function removeCoHost(roomId: string, cohostUserId: string) { await RoomRepository.removeParticipant(roomId, cohostUserId); showSuccessMessage(cohostUserId); } async function leaveAsCoHost(roomId: string) { await RoomRepository.leaveRoom(roomId); showSuccessMessage(roomId); } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient val roomRepository = AmityVideoClient.newRoomRepository() roomRepository.updateCohostPermission( roomId = roomId, cohostId = userId, canManageProductTags = true ) .andThen(roomRepository.removeRoomParticipant(roomId, userId)) .andThen(roomRepository.leaveRoom(roomId)) .subscribe( { showSuccessMessage(roomId) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS let roomRepository = AmityRoomRepository() let updatedRoom = try await roomRepository.updateCohostPermissions( roomId: roomId, cohostId: userId, canManageProductTags: true ) showSuccessMessage(updatedRoom.roomId) try await roomRepository.removeParticipant(withId: roomId, userId: userId) _ = try await roomRepository.leaveRoom(withId: roomId) showSuccessMessage(roomId) ``` TypeScript exposes room participant events as global room event subscribers, so filter by `event.room.roomId` when a screen only cares about one room. Android and iOS expose room-filtered co-host event streams from the room repository. ## Broadcaster Data Boundary Accepting a co-host invitation does not connect the app to LiveKit. After a user is ready to broadcast, request broadcaster data through the room repository and connect your media stack with the returned co-host URL and token. | Platform | Broadcaster data API | | --- | --- | | TypeScript | `RoomRepository.getBroadcasterData(roomId)` returns `coHostToken` and `coHostUrl` when available | | iOS | `AmityRoomRepository().generateRoomToken(withId:)` returns room token data | | Android | `AmityVideoClient.newRoomRepository().getBroadcasterData(roomId)` returns `AmityRoomBroadcastData.CoHosts` | | Flutter | No public room broadcasting repository found in this audit | See [Start Broadcasting](./start-broadcasting) for media connection guidance. ## Related Topics Create a co-host room and seed its initial participant list. Query, update, stop, delete, and observe rooms. Connect the accepted host or co-host to the media stack. --- ### [Recorded Room Playback](https://learn.social.plus/social-plus-sdk/video-new/broadcasting/recorded-playback) > Observe recorded room availability and pass SDK playback URLs to your player. Recorded playback becomes available after a room finishes broadcasting and the backend publishes recorded playback metadata. The social.plus SDK owns room status, recorded playback URLs, recorded thumbnails, and recorded resolution metadata. Your app owns the actual player, queueing, seek controls, buffering, and retry UI. This page covers SDK-recorded playback data. AVPlayer, ExoPlayer, HLS.js, browser autoplay handling, DRM, captions, and player UI are app-owned concerns after the SDK returns a recorded playback URL. ## Platform Surface | Platform | Observe availability | Recorded metadata | URL refresh helper | Notes | | --- | --- | --- | --- | --- | | TypeScript | `RoomRepository.getRoom(roomId, callback)` | `room.recordedPlaybackInfos[]`, `room.recordedResolution` | `RoomRepository.getRecordedUrl(roomId)` | `getRecordedUrl()` returns a URL and optional expiry timestamp. | | iOS | `AmityRoomRepository().getRoom(withId:)` | `room.recordedData[]`, `room.recordedResolution` | Re-observe or fetch the room object | Retain the returned `AmityNotificationToken` while observing. | | Android | `AmityVideoClient.newRoomRepository().getRoom(roomId)` | `room.getRecordedPlaybackInfos()`, `room.getRecordedResolution()` | `getRecordedUrls(roomId)` | `getRecordedUrls()` returns URL strings only. | | Flutter | No current public room recorded playback API found in this audit | Not available | Not available | The Flutter SDK source exposes older stream APIs, not the room broadcasting repository. | ## Parameters | Concept | TypeScript | iOS | Android | | --- | --- | --- | --- | | Observe room | `RoomRepository.getRoom(roomId, callback)` | `AmityRoomRepository().getRoom(withId:)` | `AmityVideoClient.newRoomRepository().getRoom(roomId)` | | Recorded status | `room.status === "recorded"` | `room.status == .recorded` | `room.getStatus() == AmityRoomStatus.RECORDED` | | Processing status | `room.status === "ended"` | `room.status == .ended` | `room.getStatus() == AmityRoomStatus.ENDED` | | Playback URL metadata | `room.recordedPlaybackInfos[].url` | `room.recordedData[].playbackUrl` | `room.getRecordedPlaybackInfos()[].url` | | Thumbnail metadata | `room.recordedPlaybackInfos[].thumbnailUrl` | `room.recordedData[].thumbnailUrl` | `room.getRecordedPlaybackInfos()[].thumbnailUrl` | | Recorded resolution | `room.recordedResolution` | `room.recordedResolution` | `room.getRecordedResolution()` | | Fresh URL helper | `RoomRepository.getRecordedUrl(roomId)` | Re-observe `AmityRoom` | `getRecordedUrls(roomId)` | ## Wait for Recorded Status Observe the room while the playback screen is open. Treat `ended` as a processing state and only hand a recorded URL to your player after the room status becomes `recorded`. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; function observeRecordedAvailability(roomId: string): Amity.Unsubscriber { return RoomRepository.getRoom(roomId, snapshot => { if (snapshot.error) { handleError(snapshot.error); return; } const room = snapshot.data; if (room.status === "recorded" && room.recordedPlaybackInfos.length > 0) { showSuccessMessage(room.recordedPlaybackInfos[0].url); return; } if (room.status === "ended") { showSuccessMessage("recording-processing"); return; } showSuccessMessage(room.status); }); } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient import com.amity.socialcloud.sdk.model.video.room.AmityRoomStatus val disposable = AmityVideoClient.newRoomRepository() .getRoom(roomId) .subscribe( { room -> when (room.getStatus()) { AmityRoomStatus.RECORDED -> { val firstUrl = room.getRecordedPlaybackInfos() .mapNotNull { it.url } .firstOrNull() showSuccessMessage(firstUrl ?: "recorded") } AmityRoomStatus.ENDED -> { showSuccessMessage("recording-processing") } else -> { showSuccessMessage(room.getStatus()) } } }, { error -> handleGeneralError(error) } ) ``` ```swift iOS var roomObservationToken: AmityNotificationToken? let roomObject = AmityRoomRepository().getRoom(withId: roomId) roomObservationToken = roomObject.observe { liveObject, error in if let error { handleGeneralError(error) return } guard let room = liveObject.snapshot else { return } switch room.status { case .recorded: showSuccessMessage(room.recordedData.first?.playbackUrl) case .ended: showSuccessMessage("recording-processing") default: showSuccessMessage(room.status.rawValue) } } ``` ## Read Recorded Sources Recorded metadata can contain more than one playback item. Preserve the SDK order unless your product has a backend-defined reason to reorder segments. ```typescript TypeScript type RecordedPlaybackSource = { url: string; thumbnailUrl: string; }; function recordedPlaybackSources(room: Amity.Room): RecordedPlaybackSource[] { if (room.status !== "recorded") { return []; } return room.recordedPlaybackInfos.map(info => ({ url: info.url, thumbnailUrl: info.thumbnailUrl, })); } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.video.room.AmityRoom import com.amity.socialcloud.sdk.model.video.room.AmityRoomStatus data class RecordedPlaybackSource( val url: String, val thumbnailUrl: String? ) fun recordedPlaybackSources(room: AmityRoom): List { if (room.getStatus() != AmityRoomStatus.RECORDED) { return emptyList() } return room.getRecordedPlaybackInfos().mapNotNull { info -> info.url?.let { url -> RecordedPlaybackSource( url = url, thumbnailUrl = info.thumbnailUrl ) } } } ``` ```swift iOS struct RecordedPlaybackSource { let playbackUrl: String let thumbnailUrl: String } func recordedPlaybackSources(for room: AmityRoom) -> [RecordedPlaybackSource] { guard room.status == .recorded else { return [] } return room.recordedData .filter { !$0.playbackUrl.isEmpty } .map { data in RecordedPlaybackSource( playbackUrl: data.playbackUrl, thumbnailUrl: data.thumbnailUrl ) } } ``` ## Refresh a Playback URL Refresh recorded playback data before starting playback, after a long pause, or when your player reports an expired or unauthorized media URL. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; const recordedUrl = await RoomRepository.getRecordedUrl(roomId); showSuccessMessage(recordedUrl.url); showSuccessMessage(recordedUrl.expiresAt); ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient val disposable = AmityVideoClient.newRoomRepository() .getRecordedUrls(roomId) .subscribe( { urls -> showSuccessMessage(urls.firstOrNull() ?: "no-recorded-url") }, { error -> handleGeneralError(error) } ) ``` ```swift iOS var refreshToken: AmityNotificationToken? let roomObject = AmityRoomRepository().getRoom(withId: roomId) refreshToken = roomObject.observeOnce { liveObject, error in if let error { handleGeneralError(error) return } guard let room = liveObject.snapshot, room.status == .recorded else { return } showSuccessMessage(room.recordedData.first?.playbackUrl) } ``` ## Status Handling | Room status | Recorded playback behavior | | --- | --- | | `idle` | No recording exists. Show a waiting or not-started state. | | `live` / `waitingReconnect` | Use live playback, not recorded playback. | | `ended` | Broadcast has ended, but recording metadata may still be processing. Keep observing or offer retry. | | `recorded` | Read recorded playback metadata and pass a URL to your player. | | `terminated` | TypeScript room status for a terminated room. Do not assume recorded playback is available. | | `error` | Show a recoverable error state and let the viewer retry or leave. | Do not treat `ended` as playable recorded content. Wait for `recorded` status or a successful platform URL refresh response before handing a recorded URL to the player. ## Segment and Player Boundary | Area | SDK responsibility | App/player responsibility | | --- | --- | --- | | Availability | Room status updates and recorded metadata | Processing, empty, retry, and unavailable UI | | Source list | Recorded URLs, thumbnails, and recorded resolution | Segment queueing, autoplay policy, seek controls, buffering, captions, and DRM | | Freshness | TS `getRecordedUrl()`, Android `getRecordedUrls()`, iOS room re-observation | Retry timing, expired URL recovery, and player reload | | Cleanup | Unsubscribe, invalidate tokens, or dispose Rx subscriptions | Release player instances and audio/video resources | Multiple recorded playback items can represent multiple files for one recording. Preserve the SDK order and let the player layer decide whether to play only the first URL or queue all URLs. ## Related Topics Discover room posts, observe live rooms, and choose playback sources. Fetch broadcaster credentials and start the host-side media session. Observe, query, update, stop, or delete room records. ## Video — Playback & Notifications ### [Playback Overview](https://learn.social.plus/social-plus-sdk/video-new/playback/overview) > Understand the SDK room playback contract and hand live or recorded playback URLs to your player. Playback in the social.plus SDK is a room-data workflow. The SDK gives your app room state, live playback URLs, recorded playback metadata, optional URL refresh helpers, and watch-session analytics. Your app owns the media player, autoplay policy, buffering UI, captions, DRM, seek controls, and platform playback SDK. This page covers SDK playback data for rooms. Use your own player layer, such as AVPlayer, ExoPlayer, a browser video element with HLS support, or another product-approved player, after the SDK returns a playable URL. ## Platform Surface | Platform | Observe room state | Live source | Recorded source | Recorded refresh | Watch analytics | | --- | --- | --- | --- | --- | --- | | TypeScript | `RoomRepository.getRoom(roomId, callback)` | `room.livePlaybackUrl` | `room.recordedPlaybackInfos[]` | `RoomRepository.getRecordedUrl(roomId)` | `room.analytics()` | | iOS | `AmityRoomRepository().getRoom(withId:)` | `room.livePlaybackUrl` | `room.recordedData[]` | Re-observe or fetch the room object | `room.analytics()` | | Android | `AmityVideoClient.newRoomRepository().getRoom(roomId)` | `room.getLivePlaybackUrl()` | `room.getRecordedPlaybackInfos()` | `getRecordedUrls(roomId)` | `room.analytics()` | | Flutter | No current public room playback API found in this audit | Not available | Not available | Not available | Not available | ## Playback Flow Use a room live object or room query from the platform SDK. Keep the subscription alive while the viewing screen is open. Use the live URL only when the room is `live` or `waitingReconnect`. Use recorded metadata only when the room is `recorded`. Pass the selected URL to your app-owned player. Do not call player APIs that are not part of the social.plus SDK. Create and update a room watch session from `room.analytics()` when your product considers the viewer actively watching. ## Parameters | Concept | TypeScript | iOS | Android | | --- | --- | --- | --- | | Room ID | `room.roomId` | `room.roomId` | `room.getRoomId()` | | Room status | `room.status` | `room.status` | `room.getStatus()` | | Live URL | `room.livePlaybackUrl` | `room.livePlaybackUrl` | `room.getLivePlaybackUrl()` | | Recorded URL list | `room.recordedPlaybackInfos[].url` | `room.recordedData[].playbackUrl` | `room.getRecordedPlaybackInfos()[].url` | | Recorded thumbnail list | `room.recordedPlaybackInfos[].thumbnailUrl` | `room.recordedData[].thumbnailUrl` | `room.getRecordedPlaybackInfos()[].thumbnailUrl` | | Live resolution | `room.liveResolution` | `room.liveResolution` | `room.getLiveResolution()` | | Recorded resolution | `room.recordedResolution` | `room.recordedResolution` | `room.getRecordedResolution()` | | Watch analytics | `room.analytics()` | `room.analytics()` | `room.analytics()` | ## Choose a Playback Source Select a URL from SDK room state before creating or updating your player. Return no source for statuses that are not playable. ```typescript TypeScript function playbackSourceForRoom(room: Amity.Room): string | undefined { if (room.status === "live" || room.status === "waitingReconnect") { return room.livePlaybackUrl; } if (room.status === "recorded") { return room.recordedPlaybackInfos[0]?.url; } return undefined; } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.video.room.AmityRoom import com.amity.socialcloud.sdk.model.video.room.AmityRoomStatus fun playbackSourceForRoom(room: AmityRoom): String? { return when (room.getStatus()) { AmityRoomStatus.LIVE, AmityRoomStatus.WAITING_RECONNECT -> room.getLivePlaybackUrl() AmityRoomStatus.RECORDED -> room.getRecordedPlaybackInfos() .firstOrNull() ?.url else -> null } } ``` ```swift iOS func playbackSource(for room: AmityRoom) -> String? { switch room.status { case .live, .waitingReconnect: return room.livePlaybackUrl case .recorded: return room.recordedData.first?.playbackUrl default: return nil } } ``` ## Observe Playback State Observe room state while the viewer is on a playback screen. A room can move from waiting, to live playback, to ended processing, and later to recorded playback. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; function observePlaybackState(roomId: string): Amity.Unsubscriber { return RoomRepository.getRoom(roomId, snapshot => { if (snapshot.error) { handleError(snapshot.error); return; } const room = snapshot.data; const source = room.status === "live" || room.status === "waitingReconnect" ? room.livePlaybackUrl : room.status === "recorded" ? room.recordedPlaybackInfos[0]?.url : undefined; if (source) { showSuccessMessage(source); return; } showSuccessMessage(room.status); }); } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient import com.amity.socialcloud.sdk.model.video.room.AmityRoomStatus val disposable = AmityVideoClient.newRoomRepository() .getRoom(roomId) .subscribe( { room -> when (room.getStatus()) { AmityRoomStatus.LIVE, AmityRoomStatus.WAITING_RECONNECT -> { showSuccessMessage(room.getLivePlaybackUrl() ?: "live") } AmityRoomStatus.RECORDED -> { val source = room.getRecordedPlaybackInfos() .firstOrNull() ?.url showSuccessMessage(source ?: "recorded") } else -> { showSuccessMessage(room.getStatus()) } } }, { error -> handleGeneralError(error) } ) ``` ```swift iOS var roomObservationToken: AmityNotificationToken? let roomObject = AmityRoomRepository().getRoom(withId: roomId) roomObservationToken = roomObject.observe { liveObject, error in if let error { handleGeneralError(error) return } guard let room = liveObject.snapshot else { return } switch room.status { case .live, .waitingReconnect: showSuccessMessage(room.livePlaybackUrl) case .recorded: showSuccessMessage(room.recordedData.first?.playbackUrl) default: showSuccessMessage(room.status.rawValue) } } ``` ## Refresh Recorded Playback Refresh recorded playback data before starting a recorded video, after a long pause, or when your player reports an expired or unauthorized media URL. ```typescript TypeScript import { RoomRepository } from "@amityco/ts-sdk"; async function refreshRecordedPlaybackUrl(roomId: string): Promise { const recordedUrl = await RoomRepository.getRecordedUrl(roomId); showSuccessMessage(recordedUrl.expiresAt); return recordedUrl.url; } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.video.AmityVideoClient val disposable = AmityVideoClient.newRoomRepository() .getRecordedUrls(roomId) .subscribe( { urls -> showSuccessMessage(urls.firstOrNull() ?: "no-recorded-url") }, { error -> handleGeneralError(error) } ) ``` ```swift iOS var refreshToken: AmityNotificationToken? let roomObject = AmityRoomRepository().getRoom(withId: roomId) refreshToken = roomObject.observeOnce { liveObject, error in if let error { handleGeneralError(error) return } guard let room = liveObject.snapshot, room.status == .recorded else { return } showSuccessMessage(room.recordedData.first?.playbackUrl) } ``` ## Status Handling | Room status | Playback behavior | | --- | --- | | `idle` | Show a waiting or not-started state. Do not create a player source. | | `live` | Use the live playback URL when present. | | `waitingReconnect` | Keep the viewing UI available and show reconnecting state if playback stalls. | | `ended` | Stop live playback and show processing state while recorded metadata is not ready. | | `recorded` | Use recorded playback metadata or the platform refresh helper. | | `terminated` | TypeScript room status for a terminated room. Do not assume playback is available. | | `error` | Show a recoverable error state and let the viewer retry or leave. | Do not synthesize fallback playback URLs. If the SDK room state does not include a playable live or recorded source for the current viewer, show an unavailable, blocked, processing, or retry state based on your product rules. ## SDK and Player Boundary | Area | social.plus SDK owns | Your app/player owns | | --- | --- | --- | | Discovery | Room IDs, room post data, room state, live object updates | Ranking, placement, navigation, and empty states | | Source data | Live URLs, recorded URLs, thumbnails, and resolution metadata | Player initialization, buffering, seek controls, captions, DRM, and release lifecycle | | Freshness | Platform refresh or re-observation helpers | Retry timing, expired URL recovery, and player reload | | Analytics | Room watch-session APIs | Deciding which player states count as active watch time | ## Related Topics Observe live room state and hand the live URL to your player. Wait for recorded status, read recorded metadata, and refresh URLs. Track room watch sessions after playback starts. --- ### [Livestream Analytics](https://learn.social.plus/social-plus-sdk/video-new/analytics/overview) > Track live and recorded room watch sessions with current SDK room analytics APIs. Livestream analytics track a viewer's watch session for a room. The social.plus SDK owns session creation, local watch-session storage, duration updates, and pending-session sync. Your app owns deciding when a viewer is actually watching, when playback is paused or buffering, and when a viewer changes role from viewer to host or co-host. This page covers SDK room watch-session APIs. Product analytics dashboards, custom event names, media player state, and business reporting are app-owned concerns after you create and update watch sessions. ## Platform Surface | Platform | Analytics entry point | Create session | Update duration | Sync pending sessions | | --- | --- | --- | --- | --- | | TypeScript | `room.analytics()` | `createWatchSession(startedAt)` | `updateWatchSession(sessionId, duration, endedAt)` | `syncPendingWatchSessions()` | | iOS | `room.analytics()` | `createWatchSession(startedAt:)` | `updateWatchSession(sessionId:duration:endedAt:)` | `syncPendingWatchSessions()` | | Android | `room.analytics()` | `createWatchSession(startedAt)` | `updateWatchSession(sessionId, duration, endedAt)` | `syncPendingWatchSessions()` | | Flutter | No current public room analytics API found in this audit | Not available | Not available | Not available | ## Parameters | Concept | TypeScript | iOS | Android | | --- | --- | --- | --- | | Entry point | `room.analytics()` | `room.analytics()` | `room.analytics()` | | Watchable statuses | `"live"`, `"recorded"` | `.live`, `.recorded` | `AmityRoomStatus.LIVE`, `AmityRoomStatus.RECORDED` | | Start timestamp | `Date` | `Date` | `DateTime` | | Session ID | `string` | `String` | `String` | | Duration | `number` seconds | `Int` seconds | `Long` seconds | | End timestamp | `Date` | `Date` | `DateTime` | | Sync return | `void` | `Void` | `Unit` | ## Create a Watch Session Create a watch session when the current user starts viewing a room as a viewer. The SDK accepts watch sessions only for rooms in watchable states: `live` or `recorded`. ```typescript TypeScript async function createRoomWatchSession( room: Amity.Room, ): Promise { if (room.status !== "live" && room.status !== "recorded") { return undefined; } const sessionId = await room.analytics().createWatchSession(new Date()); showSuccessMessage(sessionId); return sessionId; } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.video.room.AmityRoom import com.amity.socialcloud.sdk.model.video.room.AmityRoomStatus import org.joda.time.DateTime fun createRoomWatchSession(room: AmityRoom) { if (room.getStatus() != AmityRoomStatus.LIVE && room.getStatus() != AmityRoomStatus.RECORDED ) { return } room.analytics() .createWatchSession(DateTime.now()) .subscribe( { sessionId -> showSuccessMessage(sessionId) }, { error -> handleGeneralError(error) } ) } ``` ```swift iOS func createRoomWatchSession(for room: AmityRoom) async throws -> String? { guard room.status == .live || room.status == .recorded else { return nil } let sessionId = try await room.analytics() .createWatchSession(startedAt: Date()) showSuccessMessage(sessionId) return sessionId } ``` ## Update Watch Duration Update the session with the accumulated watch duration in seconds. Count only time your player considers watchable, such as active playback, and exclude buffering, paused, or backgrounded states according to your product rules. ```typescript TypeScript async function updateRoomWatchSession( room: Amity.Room, sessionId: string, watchedSeconds: number, ): Promise { await room.analytics().updateWatchSession( sessionId, watchedSeconds, new Date(), ); showSuccessMessage(sessionId); } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.video.room.AmityRoom import org.joda.time.DateTime fun updateRoomWatchSession( room: AmityRoom, sessionId: String, watchedSeconds: Long ) { room.analytics() .updateWatchSession( sessionId = sessionId, duration = watchedSeconds, endedAt = DateTime.now() ) .subscribe( { showSuccessMessage(sessionId) }, { error -> handleGeneralError(error) } ) } ``` ```swift iOS func updateRoomWatchSession( for room: AmityRoom, sessionId: String, watchedSeconds: Int ) async throws { try await room.analytics().updateWatchSession( sessionId: sessionId, duration: watchedSeconds, endedAt: Date() ) showSuccessMessage(sessionId) } ``` ## Sync Pending Sessions Call sync when the viewer leaves the playback experience or changes out of a viewer role. The SDK sync method handles pending locally stored sessions; your app decides the lifecycle event that should trigger it. ```typescript TypeScript function syncRoomWatchSessions(room: Amity.Room): void { room.analytics().syncPendingWatchSessions(); showSuccessMessage("sync-scheduled"); } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.video.room.AmityRoom fun syncRoomWatchSessions(room: AmityRoom) { room.analytics().syncPendingWatchSessions() showSuccessMessage("sync-scheduled") } ``` ```swift iOS func syncRoomWatchSessions(for room: AmityRoom) { room.analytics().syncPendingWatchSessions() showSuccessMessage("sync-scheduled") } ``` ## Tracking Boundaries | Area | SDK responsibility | App responsibility | | --- | --- | --- | | Session lifecycle | Create, update, and sync room watch sessions | Decide when the viewer starts and stops watching | | Watchable state | Reject sessions for non-watchable room statuses | Check room status before creating a session | | Duration | Store the latest duration value for the session | Calculate accumulated watch seconds from player state | | Sync | Send pending sessions with SDK sync behavior | Trigger sync on page exit, role transition, or product-defined lifecycle events | | Role changes | No automatic viewer/co-host policy | Stop viewer tracking before the user becomes host or co-host | Do not create watch sessions for `idle`, `ended`, `terminated`, or `error` rooms. Create sessions only after the room is `live` or `recorded`. ## Error Handling | Situation | SDK behavior | App response | | --- | --- | --- | | Non-watchable room status | Create session fails or returns an error | Check status before calling and show a non-playable state | | Missing session ID on update | Update fails because the session cannot be found | Recreate tracking only if the viewer is still watching and your product wants a new session | | Network unavailable during sync | Pending sessions stay local until sync can complete | Retry from the next appropriate lifecycle event | ## Related Topics Observe live room state before creating viewer sessions. Track recorded room playback after recording metadata is available. Stop viewer tracking when a viewer becomes a co-host. ## Chat — Overviews & Message Flagging ### [Chat Module](https://learn.social.plus/social-plus-sdk/chat/overview) > Choose the SDK surfaces for channels, members, messages, moderation, and chat engagement state. The Chat SDK is the client surface for building channel-based messaging across TypeScript, iOS, Android, and Flutter. Use it to create and query channels, manage membership, send and read messages, apply moderation actions, and render engagement state such as previews, unread counts, read receipts, and delivery status. Looking for a product walkthrough? Use [Channels & Conversations](/use-cases/chat/channels-and-conversations) and [Sending Messages](/use-cases/chat/sending-messages). Use this section when you need the exact SDK objects and methods. ## SDK Area Map | Area | Use it for | Start here | | --- | --- | --- | | Conversation management | Create, query, update, archive, and organize chat channels. | [Conversation Management](/social-plus-sdk/chat/conversation-management/overview) | | Member management | Join or leave channels, query channel members, and read membership state. | [Member Management](/social-plus-sdk/chat/conversation-management/members/overview) | | Channel governance | Add or remove members, roles, bans, and mutes. | [Channel Governance](/social-plus-sdk/chat/conversation-management/channels/governance/overview) | | Messaging features | Create, query, edit, delete, reply to, and flag messages. | [Messaging Features](/social-plus-sdk/chat/messaging-features/overview) | | Engagement features | Read message previews, unread counts, read status, delivery status, and receipt sync state. | [Unread Status Overview](/social-plus-sdk/chat/engagement-features/unread-status/overview) | | Content moderation | Connect user reports, message deletion, and channel governance into a product moderation flow. | [Content Moderation](/social-plus-sdk/chat/moderation-safety/content-moderation/overview) | ## Platform Coverage | Capability group | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Channel lifecycle | Supported | Supported | Supported | Supported | | Member operations | Supported | Supported | Supported | Supported | | Channel governance | Supported | Supported | Supported | Supported | | Message creation and querying | Supported | Supported | Supported | Supported | | Message reactions and mentions | Supported through core content APIs | Supported through core content APIs | Supported through core content APIs | Supported through core content APIs | | Message preview and unread counts | Supported with platform-specific gaps | Supported | Supported | Supported with platform-specific gaps | | Delivery receipt user queries | Supported | Supported | Supported | Not exposed in the current public Flutter SDK | | Explicit receipt sync start/stop | Supported | Supported | Supported | Not exposed in the current public Flutter SDK | Platform gaps are documented on the specific feature pages. Do not copy a code snippet from another platform when a public SDK surface is not exposed for your target platform. ## Implementation Shape Create or query the channel that owns the conversation, then resolve the target `subChannelId` for message APIs. Use member state to decide whether the current user can read, join, send, or moderate. Query messages by subchannel, apply message-type filters, and handle edits or soft deletes from the message model. Read previews, unread counters, receipt counts, and receipt sync state from the SDK where supported. ## Product Boundaries | Concern | SDK owns | Your app owns | | --- | --- | --- | | Identity | Authenticated SDK user context and channel membership state. | Login, user provisioning, and user-facing account policy. | | Rendering | Message, channel, member, receipt, and preview models. | UI layout, empty states, moderation copy, and accessibility behavior. | | Moderation | Message flagging, message deletion, and channel governance APIs. | Review queues, escalation policy, audit notes, appeal flows, and compliance workflows. | | Notifications | Data fields such as unread count, mention state, and preview content. | Push-notification templates, routing, deep links, and notification preferences. | ## Related Topics Start a community, live, or conversation channel. Send text, media, custom, and reply messages. Let users report and unreport chat messages. --- ### [Conversation Management Overview](https://learn.social.plus/social-plus-sdk/chat/conversation-management/overview) > Choose the SDK surfaces for chat channel lifecycle, member state, and channel governance. Conversation management covers the SDK calls that decide where chat happens and who can participate. Start here when your integration needs to create or find a channel, resolve a message target, manage channel membership, or build moderator tools. If you want an end-to-end product walkthrough before choosing SDK calls, read [Channels & Conversations](/use-cases/chat/channels-and-conversations). ## Capability Map | Capability | Use it for | SDK page | | --- | --- | --- | | Create channels | Create community, live, or conversation channels. | [Create Channels](./channels/create-channel) | | Get channels | Load a known channel or a set of known channel IDs. | [Get Channels](./channels/get-channel) | | Query channels | Build inboxes, discovery lists, event chat lists, and moderation channel lists. | [Query Channels](./channels/query-channels) | | Update channels | Change display name, avatar, tags, metadata, or notification mode where supported. | [Update Channels](./channels/update-channel) | | Archive channels | Archive or unarchive channels for the current user where supported. | [Archive Channels](./channels/archive-channels) | | Member operations | Join, leave, query, search, and preview channel members. | [Member Management](./members/overview) | | Channel governance | Add or remove channel members, roles, bans, and mutes. | [Channel Governance](./channels/governance/overview) | ## Platform Surface | Surface | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Create community channels | Supported | Supported | Supported | Supported | | Create live channels | Supported | Supported | Supported | Supported | | Create conversation channels | Supported | Supported | Supported | Supported | | Query by type and membership | Supported | Supported | Supported | Supported | | Query by keyword or display name | Supported | Not exposed on current query options | Not exposed on public query builder | Supported | | Get multiple known channel IDs | Supported | Supported | Supported | Not exposed in the current public Flutter repository | | Archive channels | Supported | Supported | Supported | Supported | ## Channel And Message Targets Channel APIs operate on `channelId`. Message APIs usually operate on `subChannelId`. Resolve the target subchannel from the channel or subchannel flow before creating, querying, or syncing messages. The channel pages describe channel lifecycle calls. The messaging pages describe message calls after a `subChannelId` is available. ## Implementation Shape Use community for public or private group chat, live for event-style chat, and conversation for direct or small-group chat. Use type, membership, tag, deletion, and platform-specific search filters to build channel lists. Read membership before showing composer, join, leave, or moderation actions. Use role, member, ban, and mute APIs for channel-level moderation workflows. ## Related Topics Create, query, edit, delete, reply to, and flag messages after the channel target is known. Show unread counts, read status, delivery status, and receipt sync state. Combine message reports, deletion, and channel governance into a moderation flow. --- ### [Member Management Overview](https://learn.social.plus/social-plus-sdk/chat/conversation-management/members/overview) > Choose the SDK surfaces for joining, leaving, querying, searching, and previewing chat channel members. Member management is the SDK surface for deciding who is in a channel and how your app reads that membership state. Use these pages when you need join and leave flows, member lists, mention pickers, moderator lists, or membership-aware UI. ## Capability Map | Capability | Use it for | SDK page | | --- | --- | --- | | Join a channel | Add the current user to a joinable channel. | [Join & Leave Channel](./join-leave-channel) | | Leave a channel | Remove the current user from a channel where leaving is allowed. | [Join & Leave Channel](./join-leave-channel) | | Read current membership | Hide composer controls, redirect banned users, or show join prompts. | [Join & Leave Channel](./join-leave-channel) | | Query members | Build member lists, moderator lists, banned lists, or muted lists. | [Query Members](./query-members) | | Search members | Build mention pickers and member directories. | [Query Members](./query-members) | | Preview members | Show a small channel-member sample where the platform exposes preview members. | [Preview Members](./preview-members) | ## Platform Surface | Surface | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Join channel | Supported | Supported | Supported | Supported | | Leave channel | Supported | Supported | Supported | Supported | | Current membership state | Supported | Supported | Supported | Supported | | Query members | Supported | Supported | Supported | Supported | | Search members | Supported | Supported | Supported | Supported | | Role filters | Supported | Supported | Supported | Supported | | Preview-member field | Supported | Supported | Use member-query fallback | Use member-query fallback | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `channelId` | Yes | Channel whose membership state should be read or changed. | | Membership filter | No | Filter member queries by state such as member, banned, muted, or all where supported. | | `roles` | No | Filter members by channel role, such as `moderator` or another role configured for your app. | | Search keyword | No | Search member display names where the platform exposes member search. | | `includeDeleted` | No | Include deleted users in member query results where supported. | ## Implementation Shape Read membership before showing the composer, join button, leave action, or banned-state messaging. Query members by channel, membership state, role, search keyword, and deletion state where supported. Use member search for mention pickers and user-selection flows. Use preview members where exposed; otherwise query a small member page for fallback display. ## Related Topics Assign or remove channel roles for members. Remove channel access until a user is unbanned. Restrict sending while keeping channel access. --- ### [Channel Governance Overview](https://learn.social.plus/social-plus-sdk/chat/conversation-management/channels/governance/overview) > Understand the SDK surfaces for chat channel roles, member management, bans, and mutes. Channel governance is the SDK surface for changing who can participate in a chat channel and what they can do there. Use these pages when you are building moderator tools, channel settings, admin panels, or support workflows. ## Capability Map | Capability | Purpose | SDK page | | --- | --- | --- | | Member management | Add or remove channel members. | [Member Management](./member-management) | | Role management | Add or remove channel roles for members. | [Role Management](./role-management) | | Ban management | Block users from the channel until unbanned. | [Ban Management](./ban-management) | | Mute management | Restrict users from sending messages while keeping channel access. | [Mute Management](./mute-management) | | Member queries | Read current member, role, banned, and muted state. | [Query Members](/social-plus-sdk/chat/conversation-management/members/query-members) | ## Platform Coverage | Platform | Member add/remove | Role add/remove | Ban/unban | Mute/unmute | | --- | --- | --- | --- | --- | | TypeScript | Supported | Supported | Supported | Supported | | iOS | Supported | Supported | Supported | Supported | | Android | Supported | Supported | Supported | Supported | | Flutter | Supported | Supported | Supported | Supported | Governance operations are subject to channel type, membership state, and moderation permissions. If the server rejects an operation, treat that result as authoritative and show an actionable error in your product UI. ## Implementation Shape Use the governance pages for SDK calls that change membership, roles, bans, or mutes. Use member query and search APIs to render member lists, moderator lists, muted members, and banned members. Store reason codes, escalation rules, appeals, and audit records in your own backend if your product needs them. Some governance operations are only meaningful on channel types that support the target moderation behavior. ## Related Topics Create channels before applying governance operations. Read membership, role, banned, and muted state. Moderate individual messages separately from channel membership. --- ### [Messaging Features Overview](https://learn.social.plus/social-plus-sdk/chat/messaging-features/overview) > SDK surfaces for creating, querying, updating, deleting, flagging, and engaging with chat messages. Use chat message APIs to build the message composer, message list, replies, reactions, and moderation actions inside a channel or subchannel. Most message creation APIs target a `subChannelId`; use channel and member APIs when you need to discover the correct chat target first. Send text, image, file, video, audio, and custom message payloads where the platform SDK exposes them. Load channel messages, replies, filtered message lists, and paginated message collections. Edit supported message types, soft-delete messages, and clear failed local messages where supported. Let users report or unreport messages for moderation review. ## Message Creation Coverage | Message type | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Text | `MessageRepository.createMessage` | `createTextMessage` | `createTextMessage` | `createMessage(...).text(...)` | | Image | `MessageRepository.createMessage` | `createImageMessage` | `createImageMessage` | `createMessage(...).image(...)` | | File | `MessageRepository.createMessage` | `createFileMessage` | `createFileMessage` | `createMessage(...).file(...)` | | Video | `MessageRepository.createMessage` | `createVideoMessage` | `createVideoMessage` | `createMessage(...).video(...)` | | Audio | `MessageRepository.createMessage` | `createAudioMessage` | `createAudioMessage` | No current public audio creator in the message create selector | | Custom | `MessageRepository.createMessage` | `createCustomMessage` | `createCustomMessage` | `createCustomMessage` or `createMessage(...).custom(...)` | Media-message inputs differ by platform. TypeScript creates media messages from uploaded file IDs. iOS accepts `AmityMessageAttachment` values such as `.localURL` or `.fileId`. Android accepts `AmityMessageAttachment` values such as `FILE_ID` or `URL`. Flutter creates image, file, and video messages from a `Uri`. ## Implementation Notes - Resolve the `subChannelId` from your channel flow before creating messages. - Use `parentId` when creating replies. - Use `tags` and `metadata` only for app-owned categorization or rendering context. - Upload or select media first when your target platform expects a file ID or attachment object. - Keep message state handling in the UI tied to the platform model returned by the SDK. ## Related Topics Choose the right create API for each message type. Create threaded replies with `parentId`. Query top-level messages and reply threads. Add report and unreport actions for chat messages. --- ### [Message Flagging](https://learn.social.plus/social-plus-sdk/chat/messaging-features/message-flagging) > Flag, unflag, and read the current user's flag state for chat messages. Use message flagging when a signed-in user reports a chat message. The SDKs expose three client-side actions: flag a message, unflag a message, and check whether the current user has already flagged a fetched message. Flagged messages are sent to the moderation backend. Keep product policy, review workflow, and enforcement copy outside the SDK integration layer so this page stays focused on the calls your app makes. ## Platform Surface | Platform | Flag with reason | Unflag | Check current user's flag | | --- | --- | --- | --- | | TypeScript | `MessageRepository.flagMessage(messageId, reason)` | `MessageRepository.unflagMessage(messageId)` | `MessageRepository.isMessageFlaggedByMe(messageId)` | | iOS | `messageRepository.flagMessage(withId:reason:)` | `messageRepository.unflagMessage(withId:)` | `messageRepository.isMessageFlaggedByMe(withId:)` | | Android | `messageRepository.flagMessage(messageId, reason)` | `messageRepository.unflagMessage(messageId)` | `message.isFlaggedByMe()` on a fetched `AmityMessage` | | Flutter | `messageRepository.flagMessage(messageId:reason:)` or `message.flagWithReason(reason)` | `messageRepository.unflag(messageId)` or `message.unflag()` | `message.isFlaggedByMe` on a fetched `AmityMessage` | The older Android no-reason `flagMessage(messageId)` overload and Flutter `flag(messageId)` repository method still exist for compatibility, but new docs should use reason-based flagging. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Flag | `messageId` | Yes | Message ID to report. | | Flag | `reason` | Yes | Moderation reason enum or custom reason string, depending on platform. | | Unflag | `messageId` | Yes | Message ID whose report should be removed for the current user. | | Check flag state | `messageId` | Yes | Message ID to check through the repository where supported. | | Check flag state | Fetched message object | Depends | Android and Flutter expose `isFlaggedByMe` on a fetched `AmityMessage`. | ## Flag A Message Flag a message with a moderation reason so your product can route it into review. ```typescript TypeScript import { ContentFlagReasonEnum, MessageRepository } from '@amityco/ts-sdk'; const flagged = await MessageRepository.flagMessage( messageId, ContentFlagReasonEnum.SpamOrScams, ); renderResults(flagged); ``` ```swift iOS try await messageRepository.flagMessage( withId: "message-id", reason: .spamOrScams ) showSuccessMessage("Message flagged") ``` ```kotlin Android val disposable = messageRepository .flagMessage( messageId = messageId, reason = AmityContentFlagReason.SpamOrScams, ) .subscribe( { showSuccessMessage() }, { error -> handleFlagError(error) }, ) ``` ```dart Flutter final flaggedMessage = await AmityChatClient.newMessageRepository() .flagMessage( messageId: messageId, reason: AmityContentFlagReason.spamOrScams, ); final isFlaggedByMe = flaggedMessage.isFlaggedByMe; ``` ## Use A Custom Reason Use the `Others` reason only when your UI collects additional detail from the reporter. Pass a custom reason string when the platform enum supports `Others` or direct string reasons. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const flagged = await MessageRepository.flagMessage( messageId, 'Contains sensitive account information', ); renderResults(flagged); ``` ```swift iOS try await messageRepository.flagMessage( withId: "message-id", reason: .others("Contains sensitive account information") ) ``` ```kotlin Android val disposable = messageRepository .flagMessage( messageId = messageId, reason = AmityContentFlagReason.Others( "Contains sensitive account information", ), ) .subscribe( { showSuccessMessage() }, { error -> handleFlagError(error) }, ) ``` ```dart Flutter final flaggedMessage = await AmityChatClient.newMessageRepository() .flagMessage( messageId: messageId, reason: AmityContentFlagReason.others( 'Contains sensitive account information', ), ); final flagCount = flaggedMessage.flagCount; ``` ## Unflag A Message Remove the current user's flag from a message when the user reverses the report action. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const unflagged = await MessageRepository.unflagMessage(messageId); renderResults(unflagged); ``` ```swift iOS try await messageRepository.unflagMessage(withId: "message-id") showSuccessMessage("Message unflagged") ``` ```kotlin Android val disposable = messageRepository .unflagMessage(messageId = messageId) .subscribe( { showSuccessMessage() }, { error -> handleUnflagError(error) }, ) ``` ```dart Flutter final unflaggedMessage = await AmityChatClient.newMessageRepository() .unflag(messageId); final isFlaggedByMe = unflaggedMessage.isFlaggedByMe; ``` ## Check Flag State TypeScript and iOS can ask the repository for the current user's flag state by message ID. Android and Flutter expose the state on fetched message objects. Read flag state before rendering selected or disabled report controls. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const isFlaggedByMe = await MessageRepository.isMessageFlaggedByMe(messageId); updateUI({ isFlaggedByMe }); ``` ```swift iOS let isFlaggedByMe = try await messageRepository .isMessageFlaggedByMe(withId: "message-id") showSuccessMessage(isFlaggedByMe) ``` ```kotlin Android messageRepository .getMessage(messageId = messageId) .subscribe( { fetchedMessage -> val isFlaggedByMe = fetchedMessage.isFlaggedByMe() updateMessageUI(messageId, isFlaggedByMe) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final fetchedMessage = await AmityChatClient.newMessageRepository() .getMessage(messageId); final isFlaggedByMe = fetchedMessage.isFlaggedByMe; ``` ## Related Topics Refresh list state after flag or unflag actions. Modify or soft-delete your own messages. Connect message reports to your moderation operating model. --- ### [Unread Status Overview](https://learn.social.plus/social-plus-sdk/chat/engagement-features/unread-status/overview) > Choose the correct SDK APIs for chat unread counts, read status, delivery status, and receipt sync. Unread status is the SDK surface for showing unread counts, marking messages read or delivered, and keeping receipt state current while a chat screen is active. ## Capability Map | Capability | Use it for | SDK page | | --- | --- | --- | | Channel unread count | Channel badges, total chat badge, mention indicators. | [Channel Unread Count](./channel-unread-count) | | Message read status | Mark messages read and inspect read count fields. | [Message Read Status](./message-read-status) | | Message delivery status | Mark messages delivered and query receipt users where supported. | [Message Delivery Status](./message-delivery-status) | | Message receipt sync | Subscribe to receipt updates for the active subchannel. | [Message Receipt Sync](./message-receipt-sync) | ## Platform Coverage | Capability | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Per-channel unread fields | Supported | Supported | Supported | Supported | | Total channel unread observer | Supported | Supported | Supported | Supported | | `message.markRead()` | Supported | Supported | Supported | Supported | | Mark delivered | Supported | Supported | Supported | Not exposed | | Query read users | Supported | Supported | Supported | Not exposed | | Query delivered users | Supported | Supported | Supported | Not exposed | | Explicit receipt sync start/stop | Supported | Supported | Supported | Not exposed | Platform gaps are intentional documentation facts, not missing code snippets. Add a Flutter snippet for delivered users or receipt sync only after the Flutter SDK exposes a public API for that operation. ## Implementation Shape Use channel unread fields, total unread observers, mention flags, and message previews. Start receipt sync where supported, render messages, and call `message.markRead()` when messages are seen. Use read and delivered counts, then query receipt users where the platform exposes those APIs. Stop receipt sync when a screen closes or switches to a different subchannel. ## Data Ownership | Data | Source | | --- | --- | | `unreadCount` / `isMentioned` | Channel or subchannel model returned by the SDK. | | Total unread count | SDK aggregate unread observer for the current user. | | Read and delivered counts | Message model returned by the SDK. | | Read and delivered user lists | Receipt-user query APIs where supported. | | Receipt sync state | Explicit subchannel receipt-sync APIs where supported. | ## Related Topics Show latest-message preview data beside unread state. Load message models before marking messages read. Load channel models for unread count and preview fields. --- ### [Content Moderation Overview](https://learn.social.plus/social-plus-sdk/chat/moderation-safety/content-moderation/overview) > Connect chat message reports, message removal, and channel governance with the SDK surfaces that support moderation workflows. Content moderation in the Chat SDK is built from specific client actions: users can flag messages, apps can render flag state from message models, messages can be edited or soft-deleted where supported, and moderators can use channel governance APIs for roles, bans, and mutes. Keep review policy, escalation rules, audit notes, and compliance workflows in your own product or backend. The SDK supplies the client-side actions and state needed to connect those workflows to chat. ## Capability Map | Capability | Use it for | SDK page | | --- | --- | --- | | Flag a message | Let a signed-in user report a message with a reason. | [Message Flagging](/social-plus-sdk/chat/messaging-features/message-flagging) | | Unflag a message | Let the current user remove their own report. | [Message Flagging](/social-plus-sdk/chat/messaging-features/message-flagging) | | Read flag state | Show whether the current user has already flagged a fetched message. | [Message Flagging](/social-plus-sdk/chat/messaging-features/message-flagging) | | Edit or delete messages | Update supported message types or soft-delete messages. | [Edit & Delete Messages](/social-plus-sdk/chat/messaging-features/messages/edit-and-delete-messages) | | Query deleted state | Include or exclude deleted messages where the platform exposes the filter. | [Query Messages](/social-plus-sdk/chat/messaging-features/messages/query-and-filter-messages) | | Channel governance | Manage members, roles, bans, and mutes for channel-level enforcement. | [Channel Governance](/social-plus-sdk/chat/conversation-management/channels/governance/overview) | ## Platform Surface | Surface | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Flag with reason | Supported | Supported | Supported | Supported | | Unflag | Supported | Supported | Supported | Supported | | Check current user's flag state | Supported | Supported | Supported on fetched message | Supported on fetched message | | Soft-delete messages | Supported | Supported | Supported | Supported | | Query deleted messages | Supported | Supported | Supported | Supported | | Channel bans and mutes | Supported | Supported | Supported | Supported | ## Moderation Flow Call the platform flag API with the message ID and reason selected in your UI. Re-read or observe the message when the UI needs current flag, deletion, reaction, or receipt state. Treat soft-deleted messages as deleted in your renderer and avoid showing original content. Use role, ban, mute, and member APIs when moderation affects participation rather than a single message. ## Product Boundaries | Concern | SDK surface | Product or backend responsibility | | --- | --- | --- | | User report | Message flag and unflag APIs. | Report form copy, reason taxonomy, abuse policy, and user messaging. | | Message visibility | Message delete state and deleted-message query filters. | Renderer behavior, placeholders, audit copies, and moderator review screens. | | User enforcement | Channel role, ban, mute, and member APIs. | Appeals, staff permissions, case notes, and cross-channel policy. | | Automation | Client-visible state after moderation actions. | Automated classifiers, review queues, escalation, and compliance exports. | ## Related Topics Flag, unflag, and read the current user's flag state. Update or soft-delete supported message types. Prevent users from participating in a channel until unbanned. ## Chat — Channels ### [Create Channels](https://learn.social.plus/social-plus-sdk/chat/conversation-management/channels/create-channel) > Create community, live, and conversation chat channels with the current SDK APIs. Use channel creation when your app needs a new chat container before sending messages. The current public SDK creation surface covers community, live, and conversation channels. For new integrations, let the SDK and server generate channel IDs. Custom channel IDs are not exposed on the current TypeScript, iOS, or Android creation APIs, and Flutter's custom `channelId` builder is deprecated. ## Platform Surface | Platform | Entry point | Channel types exposed here | Notes | | --- | --- | --- | --- | | TypeScript | `ChannelRepository.createChannel(...)` | `community`, `live`, `conversation` | Conversation creation is distinct by membership. | | iOS | `AmityChannelRepository().createChannel(with:)` | `AmityCommunityChannelCreateOptions`, `AmityLiveChannelCreateOptions`, `AmityConversationChannelCreateOptions` | `AmityConversationChannelCreateOptions` defaults `isDistinct` to `true`. | | Android | `AmityChatClient.newChannelRepository().createChannel(displayName)` | `.community()`, `.live()`, `.conversation(...)` | Conversation accepts one user ID or a set of user IDs. | | Flutter | `AmityChatClient.newChannelRepository().createChannel()` | `.communityType()`, `.liveType()`, `.conversationType()` | `withChannelId(...)` exists for community/live but is deprecated. | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | Channel type | Yes | Channel kind to create: community, live, or conversation. | | `displayName` | Depends | Human-readable channel name; required by Android's entry point and optional on some platform builders. | | `userIds` / `userId` | Depends | Initial members or conversation target user. Conversation creation requires at least one target user. | | `tags` | No | App-defined tags for later channel filtering. | | `metadata` | No | App-defined JSON-style metadata stored with the channel. | | `isPublic` | No | Community-channel visibility flag where exposed. | | `isDistinct` | No | Conversation de-duplication flag where exposed; iOS distinct conversations default to `true`. | ## Create A Community Channel Create a community channel when your app needs a group chat space with optional public visibility, tags, and metadata. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const { data: channel } = await ChannelRepository.createChannel({ type: 'community', displayName: 'Product support', userIds: ['user-id'], tags: ['support'], metadata: { queue: 'tier-1', }, isPublic: true, }); renderResults(channel); ``` ```swift iOS let options = AmityCommunityChannelCreateOptions() options.setDisplayName("Product support") options.setUserIds(["user-id"]) options.setTags(["support"]) options.setMetadata(["queue": "tier-1"]) options.setIsChannelPublic(true) let channel = try await channelRepository.createChannel(with: options) showSuccessMessage(channel.channelId) ``` ```kotlin Android val metadata = JsonObject().apply { addProperty("queue", "tier-1") } val disposable = channelRepository .createChannel(displayName = "Product support") .community() .userIds(listOf(targetUserId)) .tags(AmityTags(listOf("support"))) .metadata(metadata) .isPublic(true) .build() .create() .subscribe( { channel -> showSuccessMessage(channel.getChannelId()) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final channel = await AmityChatClient.newChannelRepository() .createChannel() .communityType() .withDisplayName('Product support') .userIds([targetUserId]) .tags(['support']) .metadata({'queue': 'tier-1'}) .isPublic(true) .create(); final createdChannelId = channel.channelId; ``` ## Create A Conversation Channel Conversation channels are for one-to-one or small-group private chat. Where the SDK exposes distinct conversation behavior, the default is to return the existing conversation for the same membership instead of creating a duplicate. Create a conversation channel with one or more target users, and let distinct conversation behavior prevent duplicates where supported. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const { data: conversation } = await ChannelRepository.createChannel({ type: 'conversation', userIds: ['user-id'], displayName: 'Support DM', tags: ['support'], }); renderResults(conversation); ``` ```swift iOS let options = AmityConversationChannelCreateOptions() options.setUserId("user-id") options.setDisplayName("Support DM") options.setTags(["support"]) options.setIsDistinct(true) let conversation = try await channelRepository.createChannel(with: options) showSuccessMessage(conversation.channelId) ``` ```kotlin Android val disposable = channelRepository .createChannel(displayName = "Support DM") .conversation(userId = targetUserId) .tags(AmityTags(listOf("support"))) .build() .create() .subscribe( { channel -> showSuccessMessage(channel.getChannelId()) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final conversation = await AmityChatClient.newChannelRepository() .createChannel() .conversationType() .withUserId(targetUserId) .displayName('Support DM') .tags(['support']) .create(); final channelId = conversation.channelId; ``` ## Create A Live Channel Live channels support event-style chat. The platform builders expose optional metadata, tags, and members; iOS and Android also expose live-channel linkage fields such as room or post IDs in their builders. Create a live channel for event-style chat flows such as livestreams or scheduled events. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const { data: liveChannel } = await ChannelRepository.createChannel({ type: 'live', displayName: 'Launch Q&A', userIds: ['user-id'], tags: ['event'], }); renderResults(liveChannel); ``` ```swift iOS let options = AmityLiveChannelCreateOptions() options.setDisplayName("Launch Q&A") options.setUserIds(["user-id"]) options.setTags(["event"]) let liveChannel = try await channelRepository.createChannel(with: options) showSuccessMessage(liveChannel.channelId) ``` ```kotlin Android val disposable = channelRepository .createChannel(displayName = "Launch Q&A") .live() .userIds(listOf(targetUserId)) .tags(AmityTags(listOf("event"))) .build() .create() .subscribe( { channel -> showSuccessMessage(channel.getChannelId()) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final liveChannel = await AmityChatClient.newChannelRepository() .createChannel() .liveType() .withDisplayName('Launch Q&A') .userIds([targetUserId]) .tags(['event']) .create(); final channelId = liveChannel.channelId; ``` ## Related Topics Retrieve a single channel or load known channel IDs. Build channel lists with type, membership, tag, and deletion filters. Change display name, avatar, tags, metadata, or notification mode where supported. Manage membership for channels that require explicit joining. --- ### [Get Channels](https://learn.social.plus/social-plus-sdk/chat/conversation-management/channels/get-channel) > Retrieve a single chat channel or load known channel IDs with the current SDK APIs. Use channel retrieval when your app already has a `channelId` and needs the current channel object for message routing, member preview, unread state, or display metadata. Single-channel retrieval is available on all current SDKs. Batch lookup by channel IDs is exposed on TypeScript, iOS, and Android; the current public Flutter repository does not expose a batch-by-IDs method. ## Platform Surface | Operation | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Get one channel | `ChannelRepository.getChannel(channelId, callback)` | `channelRepository.getChannel(channelId)` | `channelRepository.getChannel(channelId)` | `getChannel(channelId)` | | Result style | Live object callback | `AmityObject` | `Flowable` | `Future` | | Get channel IDs | `getChannels({ channelIds })` | `getChannels(channelIds:)` | `getChannels(channelIds)` | Not exposed in the current public repository | ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Get one channel | `channelId` | Yes | Channel ID to retrieve or observe. | | Get one channel | Callback / observer | Depends | Required by TypeScript and native live-object APIs to receive loading, error, and data updates. | | Get one channel | Unsubscriber / token / disposable | No | Handle returned by live APIs; retain it while observing and release it when done. | | Get known channel IDs | `channelIds` | Yes | List of channel IDs to resolve into channel objects where the platform exposes batch lookup. | ## Get One Channel Retrieve or observe one channel when your app already has its `channelId`. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unsubscribe = ChannelRepository.getChannel( channelId, ({ data: channel, loading, error }) => { if (error) handleError(error); if (!loading && channel) { renderResults(channel); } }, ); unsubscribe(); ``` ```swift iOS let liveChannel = channelRepository.getChannel("channel-id") token = liveChannel.observe { liveObject, error in if let error { handleError(error) return } guard let channel = liveObject.snapshot else { return } showSuccessMessage(channel.channelId) } ``` ```kotlin Android val disposable = channelRepository .getChannel(channelId = channelId) .subscribe( { channel -> showSuccessMessage(channel.getChannelId()) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final channel = await AmityChatClient.newChannelRepository() .getChannel(channelId); final fetchedChannelId = channel.channelId; ``` ## Get Known Channel IDs Use batch lookup when your app has a small list of known channel IDs and wants the matching channel objects. The collection is not the same as a general channel search; use [Query Channels](./query-channels) when you need filters or pagination. The current public Flutter `AmityChannelRepository` exposes `getChannel(channelId)` and query builders, but not a batch `getChannels(channelIds)` method. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unsubscribe = ChannelRepository.getChannels( { channelIds: [channelId, 'channel-2'], }, ({ data: channels, loading, error }) => { if (error) handleError(error); if (!loading && channels) { renderResults(channels); } }, ); unsubscribe(); ``` ```swift iOS let channelIds = ["channel-1", "channel-2"] let channels = channelRepository.getChannels(channelIds: channelIds) token = channels.observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android val channelIds = listOf(channelId, "channel-2") val disposable = channelRepository .getChannels(channelIds = channelIds) .subscribe( { channels -> showSuccessMessage(channels.size) }, { error -> handleGeneralError(error) }, ) ``` ## Related Topics Create community, live, or conversation channels. Build paginated channel lists from filters. Update channel attributes after retrieval. Render latest-message preview data from channel objects. --- ### [Query Channels](https://learn.social.plus/social-plus-sdk/chat/conversation-management/channels/query-channels) > Query chat channels by type, membership, tags, deletion state, archive state, or known channel IDs. Use channel queries to build inboxes, public channel discovery, event chat lists, and moderation views. Query support is similar across platforms, but not identical: TypeScript and Flutter expose keyword/display-name style searching, while Android's public channel query builder focuses on type, membership, tags, and deleted-state filters. ## Filter Surface | Filter | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Types | `types` | `types` | `.types(...)` or type helpers | `.types(...)` or type helpers | | Membership | `membership` | `filter` | `.filter(...)` where the builder exposes it | `.filter(...)` | | Tags | `tags`, `excludeTags` | `includingTags`, `excludingTags` | `.includingTags(...)`, `.excludingTags(...)` | `.includingTags(...)`, `.excludingTags(...)` | | Deleted state | `isDeleted` | `includeDeleted` | `.includeDeleted(...)` | `.includeDeleted(...)` | | Name or keyword | `displayName` | Not exposed on `AmityChannelQueryOptions` | Not exposed on public query builder | `.withKeyword(...)` | | Archive exclusion | `excludeArchives` | Not exposed | Not exposed | `.excludeArchives(...)` | | Sort | `sortBy` | Not exposed on current options | Not exposed on public query builder | `.sortBy(...)` with `LAST_ACTIVITY` | | Known IDs | `channelIds` | `getChannels(channelIds:)` | `getChannels(channelIds)` | Not exposed in the current public repository | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `types` | No | Channel types to include, such as community, live, or conversation. | | `membership` / `filter` | No | Restrict results by the current user's relationship to the channel. | | `tags` / `includingTags` | No | Include channels with specific app-defined tags. | | `excludeTags` / `excludingTags` | No | Exclude channels with specific app-defined tags. | | `isDeleted` / `includeDeleted` | No | Include or exclude deleted channels where the platform exposes the filter. | | `displayName` / `keyword` | No | Search by name or keyword where supported by the platform SDK. | | `excludeArchives` | No | Exclude channels archived by the current user where supported. | | `sortBy` | No | Sort channel lists where the platform exposes sort options. | | Pagination controls | No | Use collection callbacks, live collections, or paging data to load additional channels. | ## Query A Channel List Query channels with type, membership, tag, deletion, archive, search, and pagination options where each platform exposes them. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unsubscribe = ChannelRepository.getChannels( { displayName: 'support', membership: 'member', types: ['community', 'live'], tags: ['support'], excludeTags: ['hidden'], isDeleted: false, excludeArchives: true, sortBy: 'lastActivity', }, ({ data: channels, onNextPage, hasNextPage, loading, error }) => { if (error) handleError(error); if (!loading && channels) { renderResults(channels); } if (hasNextPage) onNextPage?.(); }, ); unsubscribe(); ``` ```swift iOS let options = AmityChannelQueryOptions( types: Set([AmityChannelQueryType.community, AmityChannelQueryType.live]), filter: .userIsMember, includingTags: ["support"], excludingTags: ["hidden"], includeDeleted: false ) let channels = channelRepository.getChannels(with: options) token = channels.observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android val includingTags = AmityTags().apply { add("support") } val excludingTags = AmityTags().apply { add("hidden") } val disposable = channelRepository .getChannels() .types(listOf(AmityChannel.Type.COMMUNITY, AmityChannel.Type.LIVE)) .filter(AmityChannelFilter.MEMBER) .includingTags(includingTags) .excludingTags(excludingTags) .includeDeleted(includeDeleted = false) .build() .query() .subscribe( { pagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final liveCollection = AmityChatClient.newChannelRepository() .getChannels() .withKeyword('support') .filter(AmityChannelFilter.MEMBER) .types([AmityChannelType.COMMUNITY, AmityChannelType.LIVE]) .includingTags(['support']) .excludingTags(['hidden']) .includeDeleted(false) .excludeArchives(true) .sortBy(AmityChannelSortOption.LAST_ACTIVITY) .getLiveCollection(); liveCollection.getStreamController().stream.listen((channels) { final count = channels.length; }); await liveCollection.loadNext(); ``` ## Query A Specific Type Use the type-specific helpers when the platform exposes them. Some helpers apply membership defaults: for example, Flutter's `conversationType()` and `liveType()` also set the filter to `MEMBER`. Use type-specific channel queries when your UI needs a focused list such as conversations or live channels. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unsubscribe = ChannelRepository.getChannels( { types: ['conversation'], membership: 'member', isDeleted: false, }, ({ data: channels }) => { renderResults(channels); }, ); unsubscribe(); ``` ```swift iOS let options = AmityChannelQueryOptions( types: Set([AmityChannelQueryType.conversation]), filter: .userIsMember, includeDeleted: false ) let conversations = channelRepository.getChannels(with: options) showSuccessMessage(conversations) ``` ```kotlin Android val disposable = channelRepository .getChannels() .conversationType() .includeDeleted(includeDeleted = false) .build() .query() .subscribe( { pagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final conversations = AmityChatClient.newChannelRepository() .getChannels() .conversationType() .includeDeleted(false) .excludeArchives(true) .getLiveCollection(); conversations.getStreamController().stream.listen((channels) { final count = channels.length; }); ``` ## Known IDs If you already have channel IDs, use [Get Channels](./get-channel) instead of a filtered query. Batch get-by-IDs is currently exposed on TypeScript, iOS, and Android. ## Related Topics Create a channel before it appears in query results. Retrieve a single channel or a known list of IDs. Hide or restore archived conversation channels where supported. Query members after selecting a channel. --- ### [Update Channels](https://learn.social.plus/social-plus-sdk/chat/conversation-management/channels/update-channel) > Update chat channel display name, avatar, tags, metadata, and notification mode where the SDK exposes it. Use channel updates when the app needs to change channel attributes after creation. Keep updates narrow: send only fields that actually changed, because metadata and tag updates replace the submitted values rather than merging app-side state for you. ## Update Surface | Field | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Display name | `displayName` | `setDisplayName(...)` | `.displayName(...)` | `.displayName(...)` | | Avatar | `avatarFileId` | `setAvatar(...)` | `.avatar(...)` | `.avatar(...)` | | Tags | `tags` | `setTags(...)` | `.tags(...)` | `.tags(...)` | | Metadata | `metadata` | `setMetadata(...)` | `.metadata(...)` | `.metadata(...)` | | Notification mode | `notificationMode` | Not on `AmityChannelUpdateOptions` | Not on `AmityChannelUpdate.Builder` | `.notificationMode(...)` | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `channelId` | Yes | Channel ID to update. | | `displayName` | No | Replacement channel display name. | | `avatarFileId` / `avatar` | No | Replacement avatar file reference where supported. | | `tags` | No | Replacement tag list for the channel. | | `metadata` | No | Replacement metadata stored with the channel. | | `notificationMode` | No | Channel notification mode where TypeScript and Flutter expose it. | ## Update Channel Fields Update channel profile fields such as display name, avatar, tags, or metadata where the platform exposes those setters. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const { data: updatedChannel } = await ChannelRepository.updateChannel( channelId, { displayName: 'Support desk', tags: ['support', 'active'], metadata: { queue: 'tier-2', }, }, ); renderResults(updatedChannel); ``` ```swift iOS let options = AmityChannelUpdateOptions(channelId: "channel-id") options.setDisplayName("Support desk") options.setTags(["support", "active"]) options.setMetadata(["queue": "tier-2"]) let channel = try await channelRepository.editChannel(with: options) showSuccessMessage(channel.channelId) ``` ```kotlin Android val metadata = JsonObject().apply { addProperty("queue", "tier-2") } val disposable = channelRepository .editChannel(channelId = channelId) .displayName("Support desk") .tags(AmityTags(listOf("support", "active"))) .metadata(metadata) .build() .apply() .subscribe( { channel -> showSuccessMessage(channel.getChannelId()) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final channel = await AmityChatClient.newChannelRepository() .updateChannel(channelId) .displayName('Support desk') .tags(['support', 'active']) .metadata({'queue': 'tier-2'}) .create(); final updatedChannelId = channel.channelId; ``` ## Update Notification Mode The channel update API exposes notification mode on TypeScript and Flutter. Use platform-specific notification settings pages for broader notification configuration. ```typescript TypeScript import { AmityChannelNotificationModeEnum, ChannelRepository, } from '@amityco/ts-sdk'; const { data: channel } = await ChannelRepository.updateChannel( channelId, { notificationMode: AmityChannelNotificationModeEnum.Silent, }, ); renderResults(channel); ``` ```dart Flutter final channel = await AmityChatClient.newChannelRepository() .updateChannel(channelId) .notificationMode(NotificationMode.silent) .create(); final notificationMode = channel.notificationMode; ``` ## Related Topics Observe the updated channel object. Refresh channel lists after an update. Configure channel notification settings. Manage membership separately from channel attributes. --- ### [Archive Channels](https://learn.social.plus/social-plus-sdk/chat/conversation-management/channels/archive-channels) > Archive, unarchive, and query archived chat channels where the current SDK exposes archive APIs. Archiving is a per-user chat-list operation. It hides a channel from the active list for the current user without deleting the channel or its messages. In the current public SDKs, archive APIs are exposed on TypeScript and Flutter; iOS and Android do not expose public channel archive methods in this checkout. ## Platform Surface | Operation | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Archive | `ChannelRepository.archiveChannel(channelId)` | Not exposed | Not exposed | `archiveChannel(channelId)` | | Unarchive | `ChannelRepository.unarchiveChannel(channelId)` | Not exposed | Not exposed | `unarchiveChannel(channelId)` | | Query archived channels | `ChannelRepository.getArchivedChannels(...)` | Not exposed | Not exposed | `getArchivedChannels()` | ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Archive | `channelId` | Yes | Channel ID to hide from the current user's active channel list. | | Unarchive | `channelId` | Yes | Archived channel ID to restore to the active channel list. | | Query archived channels | `limit` | No | TypeScript collection page size. | | Query archived channels | Pagination controls | No | Use TypeScript collection callbacks or Flutter live collection loading to fetch more archived channels. | ## Archive Or Unarchive Archive or unarchive a channel for the current user when the platform exposes archive state. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; await ChannelRepository.archiveChannel(channelId); await ChannelRepository.unarchiveChannel(channelId); ``` ```dart Flutter final repository = AmityChatClient.newChannelRepository(); await repository.archiveChannel(channelId); await repository.unarchiveChannel(channelId); ``` ## Query Archived Channels Use the archived-channel collection for an archived inbox. The TypeScript API accepts live collection parameters such as `limit`; the Flutter API returns a live collection stream for the current user. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unsubscribe = ChannelRepository.getArchivedChannels( { limit: 20, }, ({ data: channels, loading, error }) => { if (error) handleError(error); if (!loading && channels) { renderResults(channels); } }, ); unsubscribe(); ``` ```dart Flutter final archivedChannels = AmityChatClient.newChannelRepository() .getArchivedChannels(); archivedChannels.getStream().listen((event) { final channels = event.data; final isFetching = event.isFetching; }); await archivedChannels.loadNext(); ``` ## Notes | Topic | Current SDK guidance | | --- | --- | | Scope | Archive state belongs to the current user's archived-channel list. | | Messages | Archiving is not message deletion. Use message delete APIs for message lifecycle changes. | | Channel deletion | Archiving is separate from channel deletion or soft deletion. | | Platform gaps | Use platform checks before exposing archive UI on iOS or Android. | ## Related Topics Use `excludeArchives` where the platform query supports it. Resolve an archived channel by ID before opening it. Delete messages instead of hiding a channel. Update channel display and metadata separately from archive state. ## Chat — Channel Governance ### [Member Management](https://learn.social.plus/social-plus-sdk/chat/conversation-management/channels/governance/member-management) > Add and remove chat channel members with the current SDK APIs. Use member management when your app needs to add users to a channel or remove users from a channel. Channel type, membership rules, and moderator permissions can still determine whether a request is accepted by the server. ## Platform Surface | Operation | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Add members | `ChannelRepository.Membership.addMembers(channelId, userIds)` | `AmityChannelMembership(channelId:).addMembers(_:)` | `membership(channelId).addMembers(userIds)` | `addMembers(channelId, userIds)` | | Remove members | `ChannelRepository.Membership.removeMembers(channelId, userIds)` | `AmityChannelMembership(channelId:).removeMembers(_:)` | `membership(channelId).removeMembers(userIds)` | `removeMembers(channelId, userIds)` | | Result | `Promise` | `Void` | `Completable` | `Future` | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `channelId` | Yes | Channel ID whose membership should change. | | `userIds` | Yes | One or more user IDs to add or remove. Empty lists are rejected. | | Membership permission | Yes | The current user must be allowed to manage members for the target channel. | ## Add Members Add members when an existing channel should include additional users. For conversation channels, prefer the conversation creation APIs when creating the conversation membership for the first time. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const didAdd = await ChannelRepository.Membership.addMembers(channelId, [ userId, ]); if (didAdd) { showSuccessMessage(channelId); } ``` ```swift iOS let membership = AmityChannelMembership(channelId: channelId) try await membership.addMembers([userId]) showSuccessMessage(channelId) ``` ```kotlin Android val disposable = channelRepository .membership(channelId = channelId) .addMembers(userIds = listOf(targetUserId)) .subscribe( { showSuccessMessage(channelId) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter await AmityChatClient.newChannelRepository() .addMembers(channelId, [targetUserId]); ``` ## Remove Members Remove members when users should no longer participate in the channel. If the user is currently viewing the channel, update the route or composer state after removal succeeds. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const didRemove = await ChannelRepository.Membership.removeMembers(channelId, [ userId, ]); if (didRemove) { showSuccessMessage(channelId); } ``` ```swift iOS let membership = AmityChannelMembership(channelId: channelId) try await membership.removeMembers([userId]) showSuccessMessage(channelId) ``` ```kotlin Android val disposable = channelRepository .membership(channelId = channelId) .removeMembers(userIds = listOf(targetUserId)) .subscribe( { showSuccessMessage(channelId) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter await AmityChatClient.newChannelRepository() .removeMembers(channelId, [targetUserId]); ``` ## Implementation Notes All platforms accept a list of user IDs, so use one call for a small batch instead of looping one user at a time. Some channel types or roles can reject member changes. Treat failures as server authority, not as local state to override. Use join and leave APIs when the current user is entering or exiting a channel themselves. Query members after add or remove operations when your UI needs confirmed membership, role, or banned-state data. ## Related Topics Let the current user join or leave a channel. Retrieve and filter channel membership. Assign channel roles to members. --- ### [Role Management](https://learn.social.plus/social-plus-sdk/chat/conversation-management/channels/governance/role-management) > Add and remove chat channel roles with the current SDK APIs. Use channel roles when selected members need elevated channel permissions, such as moderator behavior. Role names are app-defined or configured for your Social+ project; the SDK applies or removes the role for the listed channel members. ## Platform Surface | Operation | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Add role | `ChannelRepository.Moderation.addRole(channelId, roleId, userIds)` | `AmityChannelModeration(channelId:).addRole(_:userIds:)` | `moderation(channelId).addRole(role, userIds)` | `moderation(channelId).addRole(role, userIds)` | | Remove role | `ChannelRepository.Moderation.removeRole(channelId, roleId, userIds)` | `AmityChannelModeration(channelId:).removeRole(_:userIds:)` | `moderation(channelId).removeRole(role, userIds)` | `moderation(channelId).removeRole(role, userIds)` | | Result | `Promise` | `Void` | `Completable` | `Future` | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `channelId` | Yes | Channel ID where the role should be changed. | | `role` / `roleId` | Yes | Role ID to add or remove, such as `moderator` or another role configured for your app. | | `userIds` | Yes | One or more member user IDs. Empty lists are rejected. | | Moderation permission | Yes | The current user must have permission to manage roles in the target channel. | ## Add A Role Add a role when channel members should receive the permissions represented by that role. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const didAddRole = await ChannelRepository.Moderation.addRole( channelId, 'moderator', [userId], ); if (didAddRole) { showSuccessMessage(channelId); } ``` ```swift iOS let moderation = AmityChannelModeration(channelId: channelId) try await moderation.addRole("moderator", userIds: [userId]) showSuccessMessage(channelId) ``` ```kotlin Android val disposable = channelRepository .moderation(channelId = channelId) .addRole(role = "moderator", userIds = listOf(targetUserId)) .subscribe( { showSuccessMessage(channelId) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter await AmityChatClient.newChannelRepository() .moderation(channelId) .addRole('moderator', [targetUserId]); ``` ## Remove A Role Remove a role when members should no longer have the permissions represented by that role. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const didRemoveRole = await ChannelRepository.Moderation.removeRole( channelId, 'moderator', [userId], ); if (didRemoveRole) { showSuccessMessage(channelId); } ``` ```swift iOS let moderation = AmityChannelModeration(channelId: channelId) try await moderation.removeRole("moderator", userIds: [userId]) showSuccessMessage(channelId) ``` ```kotlin Android val disposable = channelRepository .moderation(channelId = channelId) .removeRole(role = "moderator", userIds = listOf(targetUserId)) .subscribe( { showSuccessMessage(channelId) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter await AmityChatClient.newChannelRepository() .moderation(channelId) .removeRole('moderator', [targetUserId]); ``` ## Implementation Notes Pass the role ID string exactly as configured for your project. The SDK does not create new role definitions from this call. Assign roles to users who are channel members. Query the member list first when your UI needs to confirm eligibility. Role operations require moderation permission and can fail if the current user cannot manage the target role. Role moderation is meaningful on channel types that support member roles. Unsupported channel types can reject the request. ## Related Topics Add users before assigning channel roles. Filter members by role and membership state. Restrict access for users who should not participate. --- ### [Ban Management](https://learn.social.plus/social-plus-sdk/chat/conversation-management/channels/governance/ban-management) > Ban and unban chat channel members with the current SDK APIs. Use channel bans when a member should lose access to a specific chat channel until a moderator restores access. The SDK operation is per-channel; ban reasons, appeal flows, and moderation audit logs are app-owned product logic. ## Platform Surface | Operation | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Ban members | `ChannelRepository.Moderation.banMembers(channelId, userIds)` | `AmityChannelModeration(channelId:).banMembers(_:)` | `moderation(channelId).banMembers(userIds)` | `moderation(channelId).banMembers(userIds)` | | Unban members | `ChannelRepository.Moderation.unbanMembers(channelId, userIds)` | `AmityChannelModeration(channelId:).unbanMembers(_:)` | `moderation(channelId).unbanMembers(userIds)` | `moderation(channelId).unbanMembers(userIds)` | | Result | Cached channel memberships | `Void` | `Completable` | `Future` | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `channelId` | Yes | Channel ID where the ban or unban should be applied. | | `userIds` | Yes | One or more user IDs to ban or unban. Empty lists are rejected. | | Moderation permission | Yes | The current user must have permission to moderate the target channel. | ## Ban Members Ban removes the users from channel participation and prevents them from rejoining until they are unbanned. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const banned = await ChannelRepository.Moderation.banMembers(channelId, [ userId, ]); renderResults(banned.data); ``` ```swift iOS let moderation = AmityChannelModeration(channelId: channelId) try await moderation.banMembers([userId]) showSuccessMessage(channelId) ``` ```kotlin Android val disposable = channelRepository .moderation(channelId = channelId) .banMembers(userIds = listOf(targetUserId)) .subscribe( { showSuccessMessage(channelId) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter await AmityChatClient.newChannelRepository() .moderation(channelId) .banMembers([targetUserId]); ``` ## Unban Members Unban restores the users to the channel's allowed membership state. If your app shows a ban list, refresh that list after the operation succeeds. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unbanned = await ChannelRepository.Moderation.unbanMembers(channelId, [ userId, ]); renderResults(unbanned.data); ``` ```swift iOS let moderation = AmityChannelModeration(channelId: channelId) try await moderation.unbanMembers([userId]) showSuccessMessage(channelId) ``` ```kotlin Android val disposable = channelRepository .moderation(channelId = channelId) .unbanMembers(userIds = listOf(targetUserId)) .subscribe( { showSuccessMessage(channelId) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter await AmityChatClient.newChannelRepository() .moderation(channelId) .unbanMembers([targetUserId]); ``` ## Implementation Notes The SDK call can fail when the current user cannot moderate the channel or the target user cannot be moderated. Handle permission and validation errors in your UI. A channel ban applies to the target channel. It is not a global user block and does not replace user-level block or content flag workflows. Store ban reasons, moderator notes, appeal state, or compliance records in your own system if your product requires them. Query channel members with banned-membership filters when you need to render a ban list or confirm current ban state. ## Related Topics Filter channel members by membership state. Temporarily restrict message sending. Add or remove channel members. --- ### [Mute Management](https://learn.social.plus/social-plus-sdk/chat/conversation-management/channels/governance/mute-management) > Mute and unmute chat channel members with the current SDK APIs. Use member mute when a user should keep channel access but lose the ability to send messages for a period of time. Mute duration units differ by platform, so pass the value in the unit expected by the SDK you are using. ## Platform Surface | Operation | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Mute members | `ChannelRepository.Moderation.muteMembers(channelId, userIds, mutePeriod?)` | `AmityChannelModeration(channelId:).muteMembers(_:mutePeriod:)` | `moderation(channelId).muteMembers(timeout, userIds)` | `moderation(channelId).muteMembers(userIds, millis:)` | | Unmute members | `ChannelRepository.Moderation.unmuteMembers(channelId, userIds)` | `AmityChannelModeration(channelId:).unmuteMembers(_:)` | `moderation(channelId).unmuteMembers(userIds)` | `moderation(channelId).unmuteMembers(userIds)` | | Duration unit | Seconds; omit for indefinite mute | Seconds | `org.joda.time.Duration` | Milliseconds; defaults to 600000 | | Result | `Promise` | `Void` | `Completable` | `Future` | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `channelId` | Yes | Channel ID where the mute or unmute should be applied. | | `userIds` | Yes | One or more user IDs to mute or unmute. Empty lists are rejected. | | `mutePeriod` / `timeout` / `millis` | Mute only | Duration of the mute. TypeScript and iOS take seconds, Android takes a `Duration`, and Flutter takes milliseconds. | | Moderation permission | Yes | The current user must have permission to moderate the target channel. | ## Mute Members Mute members when you want a temporary or indefinite send-message restriction without removing channel access. TypeScript's omitted `mutePeriod` means an indefinite mute until unmuted. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const tenMinutesInSeconds = 10 * 60; const didMute = await ChannelRepository.Moderation.muteMembers( channelId, [userId], tenMinutesInSeconds, ); if (didMute) { showSuccessMessage(channelId); } ``` ```swift iOS let moderation = AmityChannelModeration(channelId: channelId) let tenMinutesInSeconds = 10 * 60 try await moderation.muteMembers([userId], mutePeriod: tenMinutesInSeconds) showSuccessMessage(channelId) ``` ```kotlin Android val tenMinutes = org.joda.time.Duration.standardMinutes(10) val disposable = channelRepository .moderation(channelId = channelId) .muteMembers(timeout = tenMinutes, userIds = listOf(targetUserId)) .subscribe( { showSuccessMessage(channelId) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter const tenMinutesInMillis = 10 * 60 * 1000; await AmityChatClient.newChannelRepository() .moderation(channelId) .muteMembers([targetUserId], millis: tenMinutesInMillis); ``` ## Unmute Members Unmute restores the users' ability to send messages in the channel. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const didUnmute = await ChannelRepository.Moderation.unmuteMembers(channelId, [ userId, ]); if (didUnmute) { showSuccessMessage(channelId); } ``` ```swift iOS let moderation = AmityChannelModeration(channelId: channelId) try await moderation.unmuteMembers([userId]) showSuccessMessage(channelId) ``` ```kotlin Android val disposable = channelRepository .moderation(channelId = channelId) .unmuteMembers(userIds = listOf(targetUserId)) .subscribe( { showSuccessMessage(channelId) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter await AmityChatClient.newChannelRepository() .moderation(channelId) .unmuteMembers([targetUserId]); ``` ## Duration Notes Pass seconds. Omit `mutePeriod` to mute indefinitely until a later `unmuteMembers` call. Pass seconds through `mutePeriod`. The SDK converts the value before sending the request. Pass an `org.joda.time.Duration`, such as `Duration.standardMinutes(10)`. Pass milliseconds through `millis`. If omitted, the current public SDK default is 600000 milliseconds. ## Related Topics Remove channel access until a user is unbanned. Filter members by muted or banned membership states. Send messages after membership and moderation checks pass. ## Chat — Members ### [Join and Leave Channels](https://learn.social.plus/social-plus-sdk/chat/conversation-management/members/join-leave-channel) > Add the current user to a chat channel, remove them from a channel, and observe current membership state with the current SDK APIs. Use join and leave operations when the current user needs to enter or exit an existing channel. Joining does not create a missing channel, and conversation channels are already membership-managed by the SDK, so calling join or leave on a conversation channel can fail on platforms that enforce that restriction. ## Platform Surface | Operation | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Join channel | `ChannelRepository.joinChannel(channelId)` | `channelRepository.joinChannel(channelId:)` | `joinChannel(channelId)` | `joinChannel(channelId)` | | Leave channel | `ChannelRepository.leaveChannel(channelId)` | `channelRepository.leaveChannel(channelId:)` | `leaveChannel(channelId)` | `leaveChannel(channelId)` | | Join result | `Promise` | `AmityChannel` | `Single` | `Future` | | Leave result | `Promise` | `Void` | `Completable` | `Future` | | Current membership | `channel.myMembership(callback)` | `channel.currentUserMembership` or `channel.myMembership()` | `membership(channelId).getMyMembership()` | `membership(channelId).getMyMembership()` | ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Join channel | `channelId` | Yes | Existing channel ID that the current user should join. | | Leave channel | `channelId` | Yes | Channel ID that the current user should leave. | | Check membership | `channelId` | Yes | Channel ID whose current-user membership should be inspected. | | Check membership | Channel object | Depends | TypeScript and iOS can also read membership state from an observed channel object. | | Check membership | Observer / disposable | Depends | Required by live APIs when observing channel or membership changes. | ## Join A Channel Join adds the current user as a member of an existing channel. On iOS, joining an already joined channel returns the existing channel. On TypeScript, the API returns `true` when the returned membership is `member`. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const didJoin = await ChannelRepository.joinChannel(channelId); if (didJoin) { showSuccessMessage(channelId); } ``` ```swift iOS let joinedChannel = try await channelRepository.joinChannel(channelId: channelId) showSuccessMessage(joinedChannel.channelId) ``` ```kotlin Android val disposable = AmityChatClient.newChannelRepository() .joinChannel(channelId = channelId) .subscribe( { joinedChannel -> showSuccessMessage(joinedChannel.getChannelId()) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final channelRepository = AmityChatClient.newChannelRepository(); await channelRepository.joinChannel(channelId); ``` ## Leave A Channel Leave removes the current user's membership from the channel. After leaving, stop routing the user into the channel UI and refresh any channel list or unread-count state that depends on membership. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const didLeave = await ChannelRepository.leaveChannel(channelId); if (didLeave) { showSuccessMessage(channelId); } ``` ```swift iOS try await channelRepository.leaveChannel(channelId: channelId) showSuccessMessage(channelId) ``` ```kotlin Android val disposable = AmityChatClient.newChannelRepository() .leaveChannel(channelId = channelId) .subscribe( { showSuccessMessage(channelId) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final channelRepository = AmityChatClient.newChannelRepository(); await channelRepository.leaveChannel(channelId); ``` ## Check Current Membership Use current membership state to hide composer controls, redirect banned users, or distinguish a channel that is only visible from one the user has joined. ```typescript TypeScript const unsubscribe = channel.myMembership(({ data: membership, error }) => { if (error) handleError(error); if (membership?.membership === 'banned') { updateUI(membership); } }); unsubscribe(); ``` ```swift iOS var token: AmityNotificationToken? token = channelRepository.getChannel(channelId).observe { liveObject, error in if let error { handleError(error) return } guard let channel = liveObject.snapshot else { return } switch channel.currentUserMembership { case .member: showSuccessMessage("member") case .banned: showSuccessMessage("banned") case .none: showSuccessMessage("none") @unknown default: break } } ``` ```kotlin Android val disposable = AmityChatClient.newChannelRepository() .membership(channelId = channelId) .getMyMembership() .subscribe( { membership -> when (membership.getMembershipType()) { AmityMembershipType.MEMBER -> showSuccessMessage("member") AmityMembershipType.BANNED -> showSuccessMessage("banned") AmityMembershipType.NONE -> showSuccessMessage("none") } }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final membership = await AmityChatClient.newChannelRepository() .membership(channelId) .getMyMembership(); final isBanned = membership.isBanned == true; ``` ## Related Topics Retrieve and filter channel members. Render lightweight participant previews where the SDK exposes them. Load channel objects before routing users into a chat screen. Handle moderation states that affect membership. --- ### [Query Channel Members](https://learn.social.plus/social-plus-sdk/chat/conversation-management/members/query-members) > Retrieve and search chat channel members with the current SDK filters, roles, deletion, and sorting APIs. Use member queries for member lists, mention pickers, moderation screens, and channel participant management. All current SDKs expose channel member query and search APIs, but the filter names are not identical across platforms. ## Platform Surface | Capability | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Query members | `ChannelRepository.Membership.getMembers(params, callback)` | `AmityChannelMembership.getMembers(...)` | `membership(channelId).getMembers()` | `membership(channelId).getMembers()` | | Search members | `searchMembers({ search })` | `searchMembers(displayName:filterBuilder:roles:)` | `searchMembers(keyword)` | `searchMembers(keyword)` | | Status filter | `memberships` | `AmityChannelMembershipFilter` | `AmityChannelMembershipFilter` | Query: `AmityChannelMembershipFilter`; search: `AmityChannelMembership` | | Role filter | `roles` | `roles` | `roles(...)` | `roles(...)` | | Include deleted users | `includeDeleted` | `includeDeleted` | `includeDeleted(...)` | `includeDeleted(...)` | | Sort | `firstCreated`, `lastCreated` | `.firstCreated`, `.lastCreated` | `FIRST_CREATED`, `LAST_CREATED` | `FIRST_CREATED`, `LAST_CREATED` | ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Query members | `channelId` | Yes | Channel ID whose members should be queried. | | Query members | `memberships` / `filter` | No | Membership status filter, such as member, banned, muted, or all depending on platform. | | Query members | `roles` | No | Channel role filters, such as `moderator`. | | Query members | `includeDeleted` | No | Whether deleted user records should be included. | | Query members | `sortBy` | No | Member ordering, such as first-created or last-created. | | Query members | `limit` / page size | No | Page size for paginated member lists. | | Search members | `search` / `displayName` / `keyword` | Yes | Search term used to find members by display name. | | Search members | `memberships` / `membershipFilter` | No | Membership status filter for search results. | | Search members | `roles` | No | Role filters applied to search results. | ## Query Members Query members when you need a paginated channel member list. The default sort is newest first on platforms that define a default. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unsubscribe = ChannelRepository.Membership.getMembers( { channelId, memberships: ['member'], roles: ['moderator'], sortBy: 'lastCreated', includeDeleted: false, limit: 20, }, ({ data: members, loading, error, hasNextPage, onNextPage }) => { if (error) handleError(error); if (!loading && members) { renderResults({ members, hasNextPage, onNextPage }); } }, ); unsubscribe(); ``` ```swift iOS var token: AmityNotificationToken? let membership = AmityChannelMembership(channelId: channelId) let members = membership.getMembers( filter: .all, sortBy: .lastCreated, roles: ["moderator"], includeDeleted: false ) token = members.observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android val disposable = AmityChatClient.newChannelRepository() .membership(channelId = channelId) .getMembers() .filter(filter = AmityChannelMembershipFilter.ALL) .roles(roles = listOf("moderator")) .includeDeleted(includeDeleted = false) .sortBy(sortOption = AmityChannelMembershipSortOption.LAST_CREATED) .build() .query() .subscribe( { members: PagingData -> showSuccessMessage(members) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final controller = PagingController( pageFuture: (token) => AmityChatClient.newChannelRepository() .membership(channelId) .getMembers() .filter(AmityChannelMembershipFilter.ALL) .roles(['moderator']) .includeDeleted(false) .sortBy(AmityMembershipSortOption.LAST_CREATED) .getPagingData(token: token, limit: 20), pageSize: 20, ); controller.fetchNextPage(); ``` ## Search Members Search members when the user enters a display-name keyword, such as in a mention picker. TypeScript names this parameter `search`; Android and Flutter name it `keyword`; iOS names it `displayName`. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unsubscribe = ChannelRepository.Membership.searchMembers( { channelId, search: 'alex', memberships: ['member'], roles: ['moderator'], includeDeleted: false, limit: 20, }, ({ data: members, error }) => { if (error) handleError(error); if (members) { renderResults(members); } }, ); unsubscribe(); ``` ```swift iOS var token: AmityNotificationToken? let membership = AmityChannelMembership(channelId: channelId) let filterBuilder = AmityChannelMembershipFilterBuilder() filterBuilder.add(filter: .member) let searchResults = membership.searchMembers( displayName: "alex", filterBuilder: filterBuilder, roles: ["moderator"], includeDeleted: false ) token = searchResults.observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.chat.member.query.AmityChannelMembership val disposable = AmityChatClient.newChannelRepository() .membership(channelId = channelId) .searchMembers(keyword = "alex") .membershipFilter(channelMembership = listOf(AmityChannelMembership.MEMBER)) .roles(roles = listOf("moderator")) .includeDeleted(includeDeleted = false) .build() .query() .subscribe( { members: PagingData -> showSuccessMessage(members) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final members = await AmityChatClient.newChannelRepository() .membership(channelId) .searchMembers('alex') .membershipFilter([AmityChannelMembership.MEMBER]) .roles(['moderator']) .includeDeleted(false) .sortBy(AmityMembershipSortOption.LAST_CREATED) .query(limit: 20); ``` ## Filter Notes TypeScript accepts `memberships: ['member' | 'banned' | 'muted']`. Android query uses `AmityChannelMembershipFilter.ALL`, `MEMBER`, or `BANNED`; Android search uses `AmityChannelMembership`. Flutter query uses `AmityChannelMembershipFilter.ALL`, `MUTED`, or `BANNED`; Flutter search uses `AmityChannelMembership`. `includeDeleted: false` filters deleted users out of the result. iOS defaults this argument to `true`, so pass `false` when you want only active user records. Role filters match channel roles assigned to members, such as `moderator` or custom roles configured for your app. TypeScript live collections expose `hasNextPage` and `onNextPage`; Android returns `PagingData`; Flutter uses `PagingController` or `getPagingData`. ## Related Topics Manage the current user's channel membership. Display a lightweight set of participant avatars. Add or remove other users from a channel. Moderate channel membership state. --- ### [Preview Channel Members](https://learn.social.plus/social-plus-sdk/chat/conversation-management/members/preview-members) > Display lightweight channel member previews where the current SDK exposes preview members. Preview members are a small set of members attached to a channel object for compact UI surfaces such as channel rows, headers, and avatar stacks. In the current public SDKs, direct channel-level preview members are exposed by TypeScript and iOS. Android and Flutter do not expose a direct `previewMembers` channel property, so use [Query Channel Members](./query-members) and render the first few results when you need the same UI pattern. ## Platform Surface | Platform | Direct preview member surface | Notes | | --- | --- | --- | | TypeScript | `channel.previewMembers` | Populated on `conversation` and `community` channels from cached channel members, up to 4 members. | | iOS | `channel.previewMembers` | Returns up to 4 members for conversation and enabled community channel previews. | | Android | Not exposed on `AmityChannel` | Query channel members and render the first few results. | | Flutter | Not exposed on `AmityChannel` | Query channel members and render the first few results. | ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Read preview members | `channelId` | Yes | Channel ID to fetch before reading preview members. | | Read preview members | `previewMembers` | Read-only | Lightweight member list exposed on supported channel objects. | | Fallback preview query | `channelId` | Yes | Channel ID whose members should be queried for preview rendering. | | Fallback preview query | `filter` | No | Membership filter for the member query. | | Fallback preview query | `includeDeleted` | No | Whether deleted users should be included in the fallback member query. | | Fallback preview query | `sortBy` | No | Member ordering for the fallback preview list. | | Fallback preview query | `limit` / page size | No | Small page size for avatar stacks or compact participant previews. | ## Read Preview Members Fetch or observe the channel first, then read the preview members from the returned channel object. Android and Flutter do not expose a direct channel-level `previewMembers` property. Query channel members and render a small first page when you need avatar-stack style previews. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unsubscribe = ChannelRepository.getChannel( channelId, ({ data: channel, loading, error }) => { if (error) handleError(error); if (!loading && channel) { renderResults(channel.previewMembers); } }, ); unsubscribe(); ``` ```swift iOS var token: AmityNotificationToken? token = channelRepository.getChannel(channelId).observe { liveObject, error in if let error { handleError(error) return } guard let channel = liveObject.snapshot else { return } let previewMembers = channel.previewMembers showSuccessMessage(previewMembers.count) } ``` ## Fallback Preview Query Use this approach on Android and Flutter, or anywhere you need filtering that the channel-level preview does not provide. ```kotlin Android val disposable = AmityChatClient.newChannelRepository() .membership(channelId = channelId) .getMembers() .filter(filter = AmityChannelMembershipFilter.ALL) .includeDeleted(includeDeleted = false) .sortBy(sortOption = AmityChannelMembershipSortOption.LAST_CREATED) .build() .query() .subscribe( { members: PagingData -> showSuccessMessage(members) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final controller = PagingController( pageFuture: (token) => AmityChatClient.newChannelRepository() .membership(channelId) .getMembers() .filter(AmityChannelMembershipFilter.ALL) .includeDeleted(false) .sortBy(AmityMembershipSortOption.LAST_CREATED) .getPagingData(token: token, limit: 4), pageSize: 4, ); controller.fetchNextPage(); ``` ## Related Topics Load the channel object that carries preview members on supported platforms. Retrieve paginated members for full member lists and fallback previews. Display latest-message previews beside member preview UI. Build channel lists that use preview members in row layouts. ## Chat — Messaging ### [Send a Message](https://learn.social.plus/social-plus-sdk/chat/messaging-features/message-creation/send-a-message) > Choose the right SDK create-message API for each chat message type. Use message creation APIs after your app has resolved the target `subChannelId`. Text and custom messages send structured data directly. Media messages send an uploaded file ID, attachment object, or file URI depending on platform. Send plain text with optional tags, metadata, mentions, and reply parent IDs. Send image messages with platform-specific file ID, attachment, or URI input. Send generic file attachments. Send video attachments where the platform SDK supports video message creation. Send audio attachments on TypeScript, iOS, and Android. Send app-defined JSON-style message payloads. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `subChannelId` | Yes | Target subchannel where the message will be created. | | Message type | Yes | SDK-specific message type selector, such as `dataType: 'text'`, `createTextMessage(...)`, or `.text(...)`. | | Message body | Depends | Required for text and custom messages; use text strings or JSON-serializable custom payloads. | | Media input | Depends | Required for media messages; use a TypeScript file ID, native attachment, or Flutter `Uri` depending on platform. | | `parentId` | No | Parent message ID when creating a reply instead of a top-level message. | | `tags` | No | App-defined tags for later filtering or grouping. | | `metadata` | No | App-defined JSON-style metadata stored with the message. | ## Send A Text Message Create a text message after resolving the target `subChannelId`. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: message } = await MessageRepository.createMessage({ subChannelId, dataType: 'text', data: { text: 'Hello from chat', }, }); renderResults(message); ``` ```swift iOS let options = AmityTextMessageCreateOptions( subChannelId: "sub-channel-id", text: "Hello from chat" ) let message = try await messageRepository.createTextMessage(options: options) showSuccessMessage(message.messageId) ``` ```kotlin Android val disposable = AmityChatClient.newMessageRepository() .createTextMessage( subChannelId = subChannelId, text = "Hello from chat", ) .build() .send() .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final message = await AmityChatClient.newMessageRepository() .createMessage(subChannelId) .text('Hello from chat') .send(); final messageId = message.messageId; ``` ## Return Shapes | Platform | Create call result | | --- | --- | | TypeScript | Resolves to `{ data: Amity.Message, cachedAt?: number }` | | iOS | Returns `AmityMessage` from the async repository call | | Android | Returns a `Completable`; observe the message list for the created message | | Flutter | Returns `Future` | ## Related Topics Add text, tags, metadata, mentions, and replies. Use `parentId` to create message replies. Observe sent messages in the target subchannel. Add report actions to sent or received messages. --- ### [Text Message](https://learn.social.plus/social-plus-sdk/chat/messaging-features/message-creation/text-message) > Send text chat messages with optional tags, metadata, mentions, and replies. Use text messages for standard chat content. All SDKs support creating text messages by `subChannelId`; optional fields such as `tags`, `metadata`, `mentionees`, and `parentId` are available through platform-specific payloads or builders. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `subChannelId` | Yes | Target subchannel where the text message will be created. | | `text` | Yes | Plain text content for the message. | | `tags` | No | App-defined tags that can be used by message queries. | | `metadata` | No | App-defined JSON-style metadata stored with the message. | | `mentionees` / `mentionUsers` | No | Users mentioned by the text message when the platform exposes mention builders. | | `parentId` | No | Parent message ID when the text message is a reply. | ## Basic Text Message Create a basic text message with a `subChannelId` and text body. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: message } = await MessageRepository.createMessage({ subChannelId, dataType: 'text', data: { text: 'Welcome to the channel', }, }); renderResults(message); ``` ```swift iOS let options = AmityTextMessageCreateOptions( subChannelId: "sub-channel-id", text: "Welcome to the channel" ) let message = try await messageRepository.createTextMessage(options: options) showSuccessMessage(message.messageId) ``` ```kotlin Android val disposable = AmityChatClient.newMessageRepository() .createTextMessage( subChannelId = subChannelId, text = "Welcome to the channel", ) .build() .send() .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final message = await AmityChatClient.newMessageRepository() .createMessage(subChannelId) .text('Welcome to the channel') .send(); final messageId = message.messageId; ``` ## Text With Context Add tags, metadata, mentions, or reply context when the text message needs extra app-owned behavior. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: message } = await MessageRepository.createMessage({ subChannelId, dataType: 'text', data: { text: 'Please review this update', }, tags: ['announcement'], metadata: { source: 'composer', }, mentionees: [{ type: 'user', userIds: [userId] }], }); ``` ```swift iOS let metadata: [String: Any] = [ "source": "composer" ] let options = AmityTextMessageCreateOptions( subChannelId: "sub-channel-id", text: "Please review this update", tags: ["announcement"], metadata: metadata ) let message = try await messageRepository.createTextMessage(options: options) showSuccessMessage(message.messageId) ``` ```kotlin Android val metadata = JsonObject().apply { addProperty("source", "composer") } val disposable = AmityChatClient.newMessageRepository() .createTextMessage( subChannelId = subChannelId, text = "Please review this update", ) .tags(AmityTags(listOf("announcement"))) .metadata(metadata) .mentionUsers(listOf(userId)) .build() .send() .subscribe() ``` ```dart Flutter final message = await AmityChatClient.newMessageRepository() .createMessage(subChannelId) .text('Please review this update') .tags(['announcement']) .metadata({'source': 'composer'}) .mentionUsers([userId]) .send(); ``` ## Related Topics Create a text reply with `parentId`. Query text messages and reply threads. --- ### [Image Message](https://learn.social.plus/social-plus-sdk/chat/messaging-features/message-creation/image-message) > Send image chat messages with platform-specific file ID, attachment, or URI inputs. Use image messages after the user selects or uploads an image. TypeScript creates image messages with a file ID. iOS and Android use `AmityMessageAttachment`. Flutter sends image messages from a `Uri`. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `subChannelId` | Yes | Target subchannel where the image message will be created. | | `fileId` / `attachment` / `Uri` | Yes | Image input for the message: uploaded file ID, platform attachment, or Flutter `Uri`. | | `caption` | No | Text caption attached to the image message where the platform exposes it. | | `fullImage` | No | iOS option for sending the full image variant. | | `parentId` | No | Parent message ID when sending the image as a reply. | | `tags` | No | App-defined tags for filtering or grouping image messages. | | `metadata` | No | App-defined metadata stored with the message. | ## Send An Image Message Create an image message from an uploaded file ID, native attachment, or Flutter image URI. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: message } = await MessageRepository.createMessage({ subChannelId, dataType: 'image', fileId, }); renderResults(message); ``` ```swift iOS let imageURL = URL(fileURLWithPath: "/tmp/photo.jpg") let options = AmityImageMessageCreateOptions( subChannelId: "sub-channel-id", attachment: .localURL(url: imageURL), caption: "Photo from today" ) let message = try await messageRepository.createImageMessage(options: options) showSuccessMessage(message.messageId) ``` ```kotlin Android val attachment = AmityMessageAttachment.FILE_ID(fileId) val disposable = AmityChatClient.newMessageRepository() .createImageMessage( subChannelId = subChannelId, attachment = attachment, ) .caption("Photo from today") .build() .send() .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final imageUri = Uri.parse('file:///tmp/photo.jpg'); final message = await AmityChatClient.newMessageRepository() .createMessage(subChannelId) .image(imageUri) .caption('Photo from today') .send(); ``` ## Optional Context Add captions, tags, metadata, or reply context when your product needs richer image-message behavior. ```swift iOS let imageURL = URL(fileURLWithPath: "/tmp/photo.jpg") let options = AmityImageMessageCreateOptions( subChannelId: "sub-channel-id", attachment: .localURL(url: imageURL), caption: "Launch photo", fullImage: true, tags: ["launch"], metadata: ["source": "composer"], parentId: "parent-message-id" ) let reply = try await messageRepository.createImageMessage(options: options) showSuccessMessage(reply.messageId) ``` ```kotlin Android val metadata = JsonObject().apply { addProperty("source", "composer") } val disposable = AmityChatClient.newMessageRepository() .createImageMessage(subChannelId, AmityMessageAttachment.FILE_ID(fileId)) .caption("Launch photo") .tags(AmityTags(listOf("launch"))) .metadata(metadata) .parentId(messageId) .build() .send() .subscribe() ``` ## Related Topics Send non-image file attachments. Send video attachments. --- ### [File Message](https://learn.social.plus/social-plus-sdk/chat/messaging-features/message-creation/file-message) > Send file chat messages with platform-specific file ID, attachment, or URI inputs. Use file messages for document and generic attachment sharing. TypeScript creates file messages from an uploaded file ID. iOS and Android use `AmityMessageAttachment`. Flutter sends file messages from a `Uri`. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `subChannelId` | Yes | Target subchannel where the file message will be created. | | `fileId` / `attachment` / `Uri` | Yes | File input for the message: uploaded file ID, platform attachment, or Flutter `Uri`. | | `caption` | No | Text caption attached to the file message where the platform exposes it. | | `parentId` | No | Parent message ID when sending the file as a reply. | | `tags` | No | App-defined tags for later filtering or grouping. | | `metadata` | No | App-defined metadata stored with the message. | ## Send A File Message Create a file message from an uploaded file ID, native attachment, or Flutter file URI. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: message } = await MessageRepository.createMessage({ subChannelId, dataType: 'file', fileId, }); renderResults(message); ``` ```swift iOS let fileURL = URL(fileURLWithPath: "/tmp/report.pdf") let options = AmityFileMessageCreateOptions( subChannelId: "sub-channel-id", attachment: .localURL(url: fileURL), fileName: "report.pdf", caption: "Monthly report" ) let message = try await messageRepository.createFileMessage(options: options) showSuccessMessage(message.messageId) ``` ```kotlin Android val attachment = AmityMessageAttachment.FILE_ID(fileId) val disposable = AmityChatClient.newMessageRepository() .createFileMessage( subChannelId = subChannelId, attachment = attachment, ) .caption("Monthly report") .build() .send() .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final fileUri = Uri.parse('file:///tmp/report.pdf'); final message = await AmityChatClient.newMessageRepository() .createMessage(subChannelId) .file(fileUri) .caption('Monthly report') .send(); ``` ## Replies And Metadata Add reply context or metadata when the file message belongs to a thread or needs app-owned rendering context. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: reply } = await MessageRepository.createMessage({ subChannelId, parentId: messageId, dataType: 'file', fileId, tags: ['attachment'], metadata: { source: 'composer', }, }); ``` ```dart Flutter final fileUri = Uri.parse('file:///tmp/report.pdf'); final reply = await AmityChatClient.newMessageRepository() .createMessage(subChannelId) .parentId(messageId) .file(fileUri) .caption('Adding the report here') .metadata({'source': 'composer'}) .send(); ``` ## Related Topics Send image attachments. Send audio attachments where supported. --- ### [Audio Message](https://learn.social.plus/social-plus-sdk/chat/messaging-features/message-creation/audio-message) > Send audio chat messages on SDKs that expose audio message creation. Use audio messages for voice notes and audio attachments. TypeScript creates audio messages from an uploaded file ID. iOS and Android use `AmityMessageAttachment`. The current Flutter public message create selector does not expose an audio creator. Flutter supports text, image, file, video, and custom message creators in the current public `createMessage` selector. Do not use a fabricated Flutter audio creator; use another supported message type or confirm audio-message support in your SDK version first. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `subChannelId` | Yes | Target subchannel where the audio message will be created. | | `fileId` / `attachment` | Yes | Uploaded audio file ID on TypeScript or platform attachment on iOS and Android. | | `fileName` | Depends | File name for iOS local URL attachments. | | `parentId` | No | Parent message ID when sending the audio message as a reply, where supported by the platform builder. | | `tags` | No | App-defined tags for message filtering, where supported by the platform builder. | | `metadata` | No | App-defined metadata stored with the message, where supported by the platform builder. | ## Send An Audio Message Create an audio message from an uploaded file ID or platform attachment, depending on the SDK. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: message } = await MessageRepository.createMessage({ subChannelId, dataType: 'audio', fileId, }); renderResults(message); ``` ```swift iOS let audioURL = URL(fileURLWithPath: "/tmp/voice.m4a") let options = AmityAudioMessageCreateOptions( subChannelId: "sub-channel-id", attachment: .localURL(url: audioURL), fileName: "voice.m4a" ) let message = try await messageRepository.createAudioMessage(options: options) showSuccessMessage(message.messageId) ``` ```kotlin Android val attachment = AmityMessageAttachment.FILE_ID(fileId) val disposable = AmityChatClient.newMessageRepository() .createAudioMessage( subChannelId = subChannelId, attachment = attachment, ) .build() .send() .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ## Related Topics Send the audio file as a generic file attachment if that fits your product behavior. Send video attachments. --- ### [Video Message](https://learn.social.plus/social-plus-sdk/chat/messaging-features/message-creation/video-message) > Send video chat messages with platform-specific file ID, attachment, or URI inputs. Use video messages for video attachments inside chat. TypeScript creates video messages from an uploaded file ID. iOS and Android use `AmityMessageAttachment`. Flutter sends video messages from a `Uri`. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `subChannelId` | Yes | Target subchannel where the video message will be created. | | `fileId` / `attachment` / `Uri` | Yes | Video input for the message: uploaded file ID, platform attachment, or Flutter `Uri`. | | `fileName` | Depends | File name for iOS local URL attachments. | | `parentId` | No | Parent message ID when sending the video as a reply, where supported by the platform builder. | | `tags` | No | App-defined tags for message filtering, where supported by the platform builder. | | `metadata` | No | App-defined metadata stored with the message, where supported by the platform builder. | ## Send A Video Message Create a video message from an uploaded file ID, native attachment, or Flutter video URI. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: message } = await MessageRepository.createMessage({ subChannelId, dataType: 'video', fileId, }); renderResults(message); ``` ```swift iOS let videoURL = URL(fileURLWithPath: "/tmp/clip.mp4") let options = AmityVideoMessageCreateOptions( subChannelId: "sub-channel-id", attachment: .localURL(url: videoURL), fileName: "clip.mp4" ) let message = try await messageRepository.createVideoMessage(options: options) showSuccessMessage(message.messageId) ``` ```kotlin Android val attachment = AmityMessageAttachment.FILE_ID(fileId) val disposable = AmityChatClient.newMessageRepository() .createVideoMessage( subChannelId = subChannelId, attachment = attachment, ) .build() .send() .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final videoUri = Uri.parse('file:///tmp/clip.mp4'); final message = await AmityChatClient.newMessageRepository() .createMessage(subChannelId) .video(videoUri) .send(); ``` ## Related Topics Send generic file attachments. Send audio attachments where supported. --- ### [Custom Message](https://learn.social.plus/social-plus-sdk/chat/messaging-features/message-creation/custom-message) > Send app-defined custom chat message payloads. Use custom messages when your app needs to send structured data that is not one of the built-in text or media message types. Keep the payload JSON-serializable and version your custom schema in your app code so older clients can render safely. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `subChannelId` | Yes | Target subchannel where the custom message will be created. | | `data` | Yes | JSON-serializable custom payload owned by your app. | | `parentId` | No | Parent message ID when sending the custom message as a reply. | | `tags` | No | App-defined tags for filtering or grouping custom messages. | | `metadata` | No | App-defined metadata stored with the message. | ## Send A Custom Message Create a custom message with a JSON-serializable payload that your app knows how to render. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: message } = await MessageRepository.createMessage({ subChannelId, dataType: 'custom', data: { kind: 'location', latitude: 13.7563, longitude: 100.5018, }, }); renderResults(message); ``` ```swift iOS let options = AmityCustomMessageCreateOptions( subChannelId: "sub-channel-id", data: [ "kind": "location", "latitude": 13.7563, "longitude": 100.5018 ] ) let message = try await messageRepository.createCustomMessage(options: options) showSuccessMessage(message.messageId) ``` ```kotlin Android val data = JsonObject().apply { addProperty("kind", "location") addProperty("latitude", 13.7563) addProperty("longitude", 100.5018) } val disposable = AmityChatClient.newMessageRepository() .createCustomMessage( subChannelId = subChannelId, data = data, ) .build() .send() .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final message = await AmityChatClient.newMessageRepository() .createCustomMessage(subChannelId, { 'kind': 'location', 'latitude': 13.7563, 'longitude': 100.5018, }) .send(); final messageId = message.messageId; ``` ## Add Tags, Metadata, Or Reply Context Attach app-owned tags, metadata, or a `parentId` when the custom message needs filtering, rendering context, or reply threading. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: reply } = await MessageRepository.createMessage({ subChannelId, parentId: messageId, dataType: 'custom', data: { kind: 'system-card', title: 'Order received', }, tags: ['system'], metadata: { source: 'checkout', }, }); ``` ```dart Flutter final reply = await AmityChatClient.newMessageRepository() .createCustomMessage(subChannelId, { 'kind': 'system-card', 'title': 'Order received', }) .parentId(messageId) .tags(['system']) .metadata({'source': 'checkout'}) .send(); ``` ## Rendering Guidance - Treat `data` as an app-owned contract. - Include a type or version field such as `kind` so clients can choose the renderer. - Provide a fallback renderer for unknown custom payloads. - Keep sensitive data out of custom payloads unless your product explicitly requires it. ## Related Topics Send plain chat content. Send custom replies with `parentId`. --- ### [Reply to a Message](https://learn.social.plus/social-plus-sdk/chat/messaging-features/message-creation/reply-to-a-message) > Create threaded chat replies by setting a parent message ID. Use `parentId` to create a reply to an existing message. The reply still targets the same `subChannelId`; `parentId` links it to the parent message for threaded rendering and reply queries. Replies are created with the same message-type APIs as top-level messages. Set `parentId` before sending the text, media, or custom message. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `subChannelId` | Yes | Target subchannel for the reply; use the same subchannel as the parent message. | | `parentId` | Yes | Message ID of the parent message being replied to. | | Message type | Yes | Text, media, or custom message type selected with the platform-specific create API. | | Message body or media input | Yes | Text/custom payload or media file input for the reply. | | `tags` | No | App-defined tags for later filtering or grouping. | | `metadata` | No | App-defined metadata stored with the reply. | ## Reply With Text Create a text reply by setting `parentId` to the message being replied to. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: reply } = await MessageRepository.createMessage({ subChannelId, parentId: messageId, dataType: 'text', data: { text: 'Replying in thread', }, }); renderResults(reply); ``` ```swift iOS let options = AmityTextMessageCreateOptions( subChannelId: "sub-channel-id", text: "Replying in thread", parentId: "parent-message-id" ) let reply = try await messageRepository.createTextMessage(options: options) showSuccessMessage(reply.messageId) ``` ```kotlin Android val disposable = AmityChatClient.newMessageRepository() .createTextMessage( subChannelId = subChannelId, text = "Replying in thread", ) .parentId(messageId) .build() .send() .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final reply = await AmityChatClient.newMessageRepository() .createMessage(subChannelId) .parentId(messageId) .text('Replying in thread') .send(); final replyId = reply.messageId; ``` ## Reply With Media Or Custom Data Use the same reply pattern with media or custom payloads when the reply is not plain text. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: imageReply } = await MessageRepository.createMessage({ subChannelId, parentId: messageId, dataType: 'image', fileId, }); ``` ```kotlin Android val disposable = AmityChatClient.newMessageRepository() .createImageMessage( subChannelId = subChannelId, attachment = AmityMessageAttachment.FILE_ID(fileId), ) .parentId(messageId) .build() .send() .subscribe() ``` ## Query Reply Threads After creating replies, query messages by `parentId` when you need to render a thread. See [Query & Filter Messages](../messages/query-and-filter-messages) for platform-specific query examples. ## Related Topics Create text messages and text replies. Query top-level messages and replies. --- ### [Edit and Delete Messages](https://learn.social.plus/social-plus-sdk/chat/messaging-features/messages/edit-and-delete-messages) > Edit text or custom chat messages and soft-delete messages after creation. Use message management APIs after your app already has the target `messageId`. The client SDKs expose text-message edits, custom-message edits, and soft deletion. Soft deletion marks a message as deleted and lets message queries or single-message observers reflect that state. It is not a hard-delete or recovery API. ## Platform Surface | Operation | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Edit text | `MessageRepository.updateMessage(messageId, patch)` | `editTextMessage(withId:_:)` | `editTextMessage(messageId).text(...).build().apply()` | `editTextMessage(messageId).text(...).update()` | | Edit custom | `MessageRepository.updateMessage(messageId, patch)` | `editCustomMessage(withId:_:)` | `editCustomMessage(messageId).data(...).build().apply()` | `editCustomMessage(messageId, data).update()` | | Soft delete | `MessageRepository.softDeleteMessage(messageId)` | `softDeleteMessage(withId:)` | `softDeleteMessage(messageId)` | `deleteMessage(messageId)` | The Flutter `updateMessage(channelId, messageId)` builder is deprecated. Use `editTextMessage(messageId)` or `editCustomMessage(messageId, customData)` for new integrations. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Edit text | `messageId` | Yes | Existing text message ID to update. | | Edit text | `text` / `data.text` | Yes | Replacement text content. | | Edit text | `tags` | No | Replacement tags where the platform edit builder exposes tags. | | Edit text | `metadata` | No | Replacement metadata stored with the message. | | Edit custom | `messageId` | Yes | Existing custom message ID to update. | | Edit custom | `data` / custom payload | Yes | Replacement JSON-serializable custom payload. | | Soft delete | `messageId` | Yes | Message ID to soft-delete. | ## Edit A Text Message Update the text body and optional app-owned context for an existing text message. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: updatedMessage } = await MessageRepository.updateMessage( messageId, { data: { text: 'Updated text' }, tags: ['edited'], metadata: { source: 'composer' }, }, ); renderResults(updatedMessage); ``` ```swift iOS try await messageRepository.editTextMessage( withId: "message-id", "Updated text", metadata: ["source": "composer"] ) showSuccessMessage("Message updated") ``` ```kotlin Android val metadata = JsonObject().apply { addProperty("source", "composer") } val disposable = messageRepository .editTextMessage(messageId = messageId) .text("Updated text") .metadata(metadata) .build() .apply() .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final updatedMessage = await AmityChatClient.newMessageRepository() .editTextMessage(messageId) .text('Updated text') .tags(['edited']) .metadata({'source': 'composer'}) .update(); final editedAt = updatedMessage.editedAt; ``` ## Edit A Custom Message Update the JSON-serializable payload for an existing custom message. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: updatedMessage } = await MessageRepository.updateMessage( messageId, { data: { value: { state: 'confirmed' }, }, }, ); renderResults(updatedMessage); ``` ```swift iOS try await messageRepository.editCustomMessage( withId: "message-id", ["state": "confirmed"] ) showSuccessMessage("Custom message updated") ``` ```kotlin Android val customData = JsonObject().apply { addProperty("state", "confirmed") } val disposable = messageRepository .editCustomMessage(messageId = messageId) .data(customData) .build() .apply() .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final updatedMessage = await AmityChatClient.newMessageRepository() .editCustomMessage(messageId, {'state': 'confirmed'}) .update(); final editedAt = updatedMessage.editedAt; ``` ## Soft-Delete A Message Soft-delete a message by ID so list and single-message observers can reflect deleted state. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const deletedMessage = await MessageRepository.softDeleteMessage(messageId); renderResults(deletedMessage); ``` ```swift iOS try await messageRepository.softDeleteMessage(withId: "message-id") showSuccessMessage("Message deleted") ``` ```kotlin Android val disposable = messageRepository .softDeleteMessage(messageId = messageId) .subscribe( { showSuccessMessage() }, { error -> handleDeletionError(error) }, ) ``` ```dart Flutter await AmityChatClient.newMessageRepository() .deleteMessage(messageId); ``` ## After Edit Or Delete - Observe the message or query collection again instead of mutating app UI state by hand. - Use `editedAt` to render edited indicators after an edit. - Use `isDeleted` to render deleted placeholders or hide deleted messages depending on your product rule. - Pass `includeDeleted` on query APIs only where the platform exposes that filter. ## Related Topics Create the message before editing or deleting it. Observe one message after an edit or delete. Refresh timelines and reply threads. --- ### [Get and View a Message](https://learn.social.plus/social-plus-sdk/chat/messaging-features/messages/get-and-view-a-message) > Retrieve one chat message by ID and inspect its SDK fields. Use a single-message lookup when your app needs to open a message permalink, refresh a message after an action, or observe a specific message for edits, deletion, reactions, flags, and marker updates. The SDK returns the same message model used by message queries. Rendering is app-owned: inspect `messageType` or `dataType`, read the typed data payload, and handle `isDeleted` before displaying content. ## Platform Surface | Platform | Retrieval shape | Update behavior | | --- | --- | --- | | TypeScript | `MessageRepository.getMessage(messageId, callback)` | Live object callback; returns unsubscriber | | iOS | `messageRepository.getMessage(messageId)` | `AmityObject` observation | | Android | `messageRepository.getMessage(messageId)` | `Flowable` | | Flutter | `messageRepository.getMessage(messageId)` | One-shot `Future`; use `message.listen` for local live updates | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `messageId` | Yes | Message ID to retrieve or observe. | | Callback / observer | Depends | Required by TypeScript and native live-object APIs to receive loading, error, and data updates. | | Unsubscriber / token / disposable | No | Handle returned by live APIs; retain it while observing and release it when the UI no longer needs updates. | ## Retrieve A Message Retrieve or observe a single message by `messageId`. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const unsubscribe = MessageRepository.getMessage( messageId, ({ data: fetchedMessage, loading, error }) => { if (error) { handleError(error); return; } if (!loading && fetchedMessage) { renderResults(fetchedMessage); } }, ); unsubscribe(); ``` ```swift iOS let liveMessage = messageRepository.getMessage("message-id") token = liveMessage.observe { liveObject, error in if let error { handleError(error) return } if let message = liveObject.snapshot { showSuccessMessage(message.messageId) } } ``` ```kotlin Android val disposable = messageRepository .getMessage(messageId = messageId) .subscribe( { fetchedMessage -> showSuccessMessage(fetchedMessage.getMessageId()) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final fetchedMessage = await AmityChatClient.newMessageRepository() .getMessage(messageId); final fetchedMessageId = fetchedMessage.messageId; final isDeleted = fetchedMessage.isDeleted ?? false; ``` ## Inspect Message Content Read the message type and data payload before choosing the renderer for your UI. ```typescript TypeScript if (message.dataType === 'text') { const text = message.data?.text; renderResults(text); } if (message.dataType === 'image') { const fileId = message.data?.fileId; renderResults(fileId); } ``` ```swift iOS let liveMessage = messageRepository.getMessage("message-id") token = liveMessage.observe { liveObject, _ in guard let message = liveObject.snapshot else { return } switch message.messageType { case .text: showSuccessMessage(message.messageId) case .image, .file, .video, .audio, .custom: showSuccessMessage(message.messageId) } } ``` ```kotlin Android messageRepository .getMessage(messageId = messageId) .subscribe { fetchedMessage -> when (fetchedMessage.getData()) { is AmityMessage.Data.TEXT -> showSuccessMessage() is AmityMessage.Data.IMAGE -> showSuccessMessage() is AmityMessage.Data.FILE -> showSuccessMessage() is AmityMessage.Data.VIDEO -> showSuccessMessage() is AmityMessage.Data.AUDIO -> showSuccessMessage() is AmityMessage.Data.CUSTOM -> showSuccessMessage() else -> showSuccessMessage() } } ``` ```dart Flutter final fetchedMessage = await AmityChatClient.newMessageRepository() .getMessage(messageId); switch (fetchedMessage.dataType) { case AmityMessageDataType.TEXT: case AmityMessageDataType.IMAGE: case AmityMessageDataType.FILE: case AmityMessageDataType.VIDEO: case AmityMessageDataType.AUDIO: case AmityMessageDataType.CUSTOM: final fetchedMessageId = fetchedMessage.messageId; break; } ``` ## Deletion And Edit Fields | Field | What to use it for | | --- | --- | | `isDeleted` | Decide whether to hide content or render a deleted-message placeholder. | | `editedAt` | Show an edited indicator when present. | | `parentId` | Link a reply back to its parent message. | | `childrenNumber` | Show reply/thread counts when available. | | `flagCount` / `isFlaggedByMe` | Render report state and moderation affordances. | Single-message retrieval does not replace list queries. Use `getMessages` for timelines and use `getMessage` for detail refresh, deep links, or focused observation. ## Related Topics Load message lists and thread replies. Update or soft-delete a message after sending. Report and unreport chat messages. --- ### [Query and Filter Messages](https://learn.social.plus/social-plus-sdk/chat/messaging-features/messages/query-and-filter-messages) > Query chat messages by subchannel, type, tags, deleted state, parent message, or around-message context. Use message queries to build timelines, reply threads, media views, and jump-to-message flows. All platforms start from a `subChannelId`; optional filters narrow the result set. For chat timelines, prefer live collections or reactive streams where the platform provides them. They keep edits, deletes, reactions, and newly created messages in sync without a manual refresh loop. ## Filter Surface | Filter | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Subchannel | `subChannelId` | `subChannelId` | `getMessages(subChannelId)` | `getMessages(subChannelId)` | | Tags | `includingTags`, `excludingTags` | `includingTags`, `excludingTags` | `includingTags()`, `excludingTags()` | `includingTags()`, `excludingTags()` | | Type | `type` | `type` | `type()` | `type()` | | Deleted messages | `includeDeleted` | Not exposed on query options | `includeDeleted()` | `includeDeleted()` | | Replies | `parentId` | `messageParentFilter` | `parentId()` | `parentId()` | | Jump context | `aroundMessageId` | `aroundMessageId` | `aroundMessageId()` | `aroundMessageId()` | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `subChannelId` | Yes | Target subchannel whose messages should be queried. | | `includingTags` / `excludingTags` | No | Include or exclude messages with specific app-defined tags. | | `type` | No | Restrict results to one message type, such as text, image, file, video, audio, or custom. | | `includeDeleted` | No | Include soft-deleted messages where the platform exposes the filter. | | `parentId` / `messageParentFilter` | No | Query replies for a parent message instead of the main timeline. | | `aroundMessageId` | No | Load messages around a target message for deep-link or jump-to-message flows. | | `sortBy` / `stackFromEnd` | No | Control timeline ordering where the platform exposes sort options. | | Pagination controls | No | Use callbacks, live collections, or paging data to load additional results. | ## Query A Timeline Query the main message timeline for a subchannel with optional tag and sort filters. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const unsubscribe = MessageRepository.getMessages( { subChannelId, includingTags: ['public'], excludingTags: ['hidden'], sortBy: 'segmentDesc', }, ({ data: messages, onNextPage, hasNextPage, loading, error }) => { if (error) handleError(error); if (!loading && messages) { renderResults(messages); } if (hasNextPage) onNextPage?.(); }, ); unsubscribe(); ``` ```swift iOS let options = AmityMessageQueryOptions( subChannelId: "sub-channel-id", includingTags: ["public"], excludingTags: ["hidden"], sortOption: .lastCreated ) let messages = messageRepository.getMessages(options: options) token = messages.observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android val includingTags = AmityTags().apply { add("public") } val excludingTags = AmityTags().apply { add("hidden") } val disposable = messageRepository .getMessages(subChannelId = subChannelId) .includingTags(includingTags) .excludingTags(excludingTags) .sortBy(AmityMessageQuerySortOption.LAST_CREATED) .build() .query() .subscribe( { pagingData -> getPagingData(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final liveCollection = AmityChatClient.newMessageRepository() .getMessages(subChannelId) .includingTags(['public']) .excludingTags(['hidden']) .stackFromEnd(true) .getLiveCollection(); liveCollection.getStreamController().stream.listen((messages) { final count = messages.length; }); liveCollection.loadNext(); ``` ## Query By Type Or Deleted State Use message type and deleted-state filters to build focused views such as media galleries or moderation queues. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const unsubscribe = MessageRepository.getMessages( { subChannelId, type: 'image', includeDeleted: false, }, ({ data: messages }) => { renderResults(messages); }, ); unsubscribe(); ``` ```swift iOS let options = AmityMessageQueryOptions( subChannelId: "sub-channel-id", type: .image, sortOption: .lastCreated ) let messages = messageRepository.getMessages(options: options) showSuccessMessage(messages) ``` ```kotlin Android val disposable = messageRepository .getMessages(subChannelId = subChannelId) .type(AmityMessage.DataType.IMAGE) .includeDeleted(includeDeleted = false) .build() .query() .subscribe( { pagingData -> getPagingData(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final imageMessages = await AmityChatClient.newMessageRepository() .getMessages(subChannelId) .type(AmityMessageDataType.IMAGE) .includeDeleted(false) .query(); final count = imageMessages.length; ``` ## Query Replies Use a parent message ID to fetch replies to that message. Use the no-parent/default query for the main timeline. Query replies with `parentId` when rendering a message thread. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const unsubscribe = MessageRepository.getMessages( { subChannelId, parentId: messageId, sortBy: 'segmentAsc', }, ({ data: replies }) => { renderResults(replies); }, ); unsubscribe(); ``` ```swift iOS let options = AmityMessageQueryOptions( subChannelId: "sub-channel-id", messageParentFilter: .parent(id: "parent-message-id"), sortOption: .firstCreated ) let replies = messageRepository.getMessages(options: options) showSuccessMessage(replies) ``` ```kotlin Android val disposable = messageRepository .getMessages(subChannelId = subChannelId) .parentId(parentId = messageId) .sortBy(AmityMessageQuerySortOption.FIRST_CREATED) .build() .query() .subscribe( { pagingData -> getPagingData(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final replies = await AmityChatClient.newMessageRepository() .getMessages(subChannelId) .parentId(messageId) .query(); final replyCount = replies.length; ``` ## Jump To A Message Use `aroundMessageId` when your app opens a deep link or search result and needs the target message plus nearby context. Query around a target message to load nearby context for deep links and search results. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const unsubscribe = MessageRepository.getMessages( { subChannelId, aroundMessageId: messageId, }, ({ data: messages, hasPrevPage, hasNextPage }) => { renderResults({ messages, hasPrevPage, hasNextPage }); }, ); unsubscribe(); ``` ```swift iOS let options = AmityMessageQueryOptions( subChannelId: "sub-channel-id", aroundMessageId: "message-id", sortOption: .lastCreated ) let messages = messageRepository.getMessages(options: options) showSuccessMessage(messages) ``` ```kotlin Android val disposable = messageRepository .getMessages(subChannelId = subChannelId) .aroundMessageId(messageId = messageId) .build() .query() .subscribe( { pagingData -> getPagingData(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final messagesAroundTarget = await AmityChatClient.newMessageRepository() .getMessages(subChannelId) .aroundMessageId(messageId) .query(); final count = messagesAroundTarget.length; ``` ## Related Topics Create messages that appear in the queried subchannel. Retrieve or observe one message by ID. Update queried messages after creation. ## Chat — Engagement ### [Message Preview](https://learn.social.plus/social-plus-sdk/chat/engagement-features/message-preview) > Read the latest message preview from chat channel and subchannel SDK objects. Use message preview when your app needs a lightweight latest-message summary for a chat list, inbox row, or notification handoff. The SDK exposes preview data on channel and subchannel models after message preview is enabled for the network. Message preview is configured outside these client SDK calls, typically through network settings. Client SDKs read the preview that is returned with channel or subchannel data; they do not enable the setting themselves. ## Platform Surface | Surface | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Channel preview | `channel.messagePreview` | `channel.messagePreview` | `channel.getMessagePreview()` | `channel.messagePreview` | | Subchannel preview | `subChannel.messagePreview` | `subChannel.messagePreview` | `subChannel.getMessagePreview()` | Not exposed as a public preview object | | Preview ID | `messagePreviewId` | `messagePreviewId` | `getMessagePreviewId()` | `messagePreviewId` | | Preview data | `data`, `dataType` | `data`, `dataType` | `getData()`, `getDataType()` | `data`, `dataType` | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `channelId` | Yes | Channel ID used to retrieve or observe the channel that contains the preview. | | `subChannelId` | Yes for subchannel preview | Subchannel ID used to retrieve or observe a subchannel preview where the platform exposes it. | | Preview setting | Yes | The network must have message preview enabled before new messages produce preview data. | | `messagePreview` null handling | Yes | A channel or subchannel can return no preview when no eligible message exists, preview is disabled, or the preview has not been cached yet. | ## Read Channel Preview Read the preview from the channel object you already use for channel lists. Treat the preview as optional and render a fallback for empty channels. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unsubscribe = ChannelRepository.getChannel( channelId, ({ data: channelSnapshot, loading, error }) => { if (error) { handleError(error); return; } if (!loading && channelSnapshot?.messagePreview) { renderResults({ id: channelSnapshot.messagePreview.messagePreviewId, dataType: channelSnapshot.messagePreview.dataType, data: channelSnapshot.messagePreview.data, userId: channelSnapshot.messagePreview.user?.userId, }); } }, ); unsubscribe(); ``` ```swift iOS token = channelRepository.getChannel(channelId).observe { liveObject, error in if let error { handleError(error) return } guard let preview = liveObject.snapshot?.messagePreview else { return } showSuccessMessage([ "id": preview.messagePreviewId, "type": preview.dataType, "subChannelId": preview.subChannelId ]) } ``` ```kotlin Android val currentChannel = channel ?: return val preview = currentChannel.getMessagePreview() ?: return showSuccessMessage( mapOf( "id" to preview.getMessagePreviewId(), "type" to preview.getDataType(), "subChannelId" to preview.getSubChannelId(), ), ) ``` ```dart Flutter final fetchedChannel = await AmityChatClient.newChannelRepository() .getChannel(channelId); final preview = fetchedChannel.messagePreview; if (preview != null) { final previewId = preview.messagePreviewId; final previewType = preview.dataType; final previewData = preview.data; } ``` ## Read Subchannel Preview Use subchannel preview when your UI lists subchannels directly. Flutter currently exposes `messagePreviewId` on `AmitySubChannel`, but not the composed `AmityMessagePreview` object. ```typescript TypeScript import { SubChannelRepository } from '@amityco/ts-sdk'; const unsubscribe = SubChannelRepository.getSubChannel( subChannelId, ({ data: subChannel, loading, error }) => { if (error) { handleError(error); return; } if (!loading && subChannel?.messagePreview) { renderResults(subChannel.messagePreview.messagePreviewId); } }, ); unsubscribe(); ``` ```swift iOS let subChannelRepository = AmitySubChannelRepository() token = subChannelRepository.getSubChannel(withId: "sub-channel-id").observe { liveObject, error in if let error { handleError(error) return } guard let preview = liveObject.snapshot?.messagePreview else { return } showSuccessMessage(preview.messagePreviewId) } ``` ```kotlin Android val subChannelRepository = AmityChatClient.newSubChannelRepository() val disposable = subChannelRepository .getSubChannel(subChannelId) .subscribe( { subChannel -> val preview = subChannel.getMessagePreview() showSuccessMessage(preview?.getMessagePreviewId() ?: "") }, { error -> handleGeneralError(error) }, ) ``` ## Preview Fields | Field | Description | | --- | --- | | `messagePreviewId` | ID of the message represented by the preview. | | `dataType` | Message data type, such as text, image, file, video, audio, or custom. | | `data` | Preview payload for the message data type. For text messages, this contains the text payload. | | `channelId` | Channel that owns the previewed message. | | `subChannelId` | Subchannel that owns the previewed message. | | `subChannelName` | Subchannel display name where the platform exposes it. | | `isDeleted` | Whether the previewed message is deleted. Availability depends on the network preview setting. | | `user` | User object for the previewed message creator when the SDK has the user data. | ## Related Topics Load channel lists that include preview data. Load full message timelines when preview data is not enough. Combine previews with unread count and mention indicators. --- ### [Channel Unread Count](https://learn.social.plus/social-plus-sdk/chat/engagement-features/unread-status/channel-unread-count) > Read per-channel and total chat unread counts from SDK channel objects. Use channel unread count when your app needs inbox badges, mention indicators, or total chat unread state. The SDK exposes unread state on channel models and provides an aggregate total for channels known to the current user. ## Platform Surface | Surface | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Channel count | `channel.unreadCount` | `channel.unreadCount` | `channel.getUnreadCount()` | `channel.unreadCount` | | Channel mention | `channel.isMentioned` | `channel.isMentioned` | `channel.isMentioned()` | `channel.isMentioned` | | Support flag | `channel.isUnreadCountSupport` | `channel.isUnreadCountSupported` | `channel.isUnreadCountSupport()` | Not exposed | | Subchannel aggregate on channel | `channel.subChannelsUnreadCount` | `channel.subChannelsUnreadCount` | `channel.getSubChannelsUnreadCount()` | Not exposed | | Total channel unread | `ChannelRepository.getTotalChannelsUnread(callback)` | `channelRepository.getTotalChannelsUnread()` | `channelRepository.getTotalChannelUnread()` | `getChannelTotalUnreads()` | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `channelId` | Yes for single-channel reads | Channel ID used to retrieve or observe a channel model. | | Channel object | Yes | Read `unreadCount` and `isMentioned` from the latest channel model returned by the SDK. | | Aggregate observer | Yes for total unread | Subscribe to the total-unread API for cross-channel badge state. | | Unsubscriber / token / disposable | Yes for live observers | Keep the returned handle while observing and release it when the UI no longer needs updates. | ## Read Channel Unread State Read unread count and mention state from the channel object. If the platform exposes a support flag, check it before showing unread count for channel types that do not support markers. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unsubscribe = ChannelRepository.getChannel( channelId, ({ data: channelSnapshot, loading, error }) => { if (error) { handleError(error); return; } if (!loading && channelSnapshot?.isUnreadCountSupport) { renderResults({ unreadCount: channelSnapshot.unreadCount, isMentioned: channelSnapshot.isMentioned, subChannelsUnreadCount: channelSnapshot.subChannelsUnreadCount, }); } }, ); unsubscribe(); ``` ```swift iOS token = channelRepository.getChannel(channelId).observe { liveObject, error in if let error { handleError(error) return } guard let channel = liveObject.snapshot else { return } if channel.isUnreadCountSupported { showSuccessMessage([ "unreadCount": channel.unreadCount, "isMentioned": channel.isMentioned, "subChannelsUnreadCount": channel.subChannelsUnreadCount ]) } } ``` ```kotlin Android val currentChannel = channel ?: return if (currentChannel.isUnreadCountSupport()) { showSuccessMessage( mapOf( "unreadCount" to currentChannel.getUnreadCount(), "isMentioned" to currentChannel.isMentioned(), "subChannelsUnreadCount" to currentChannel.getSubChannelsUnreadCount(), ), ) } ``` ```dart Flutter final fetchedChannel = await AmityChatClient.newChannelRepository() .getChannel(channelId); final unreadCount = fetchedChannel.unreadCount ?? 0; final isMentioned = fetchedChannel.isMentioned; ``` ## Observe Total Channel Unread Use total unread APIs for app-level badges or global chat navigation. The aggregate contains the unread count and whether any unread message mentions the current user. The total unread count is calculated from the channels the SDK has already synced into its local cache. It is a live observation, not a one-time server fetch, so it starts at `0` and only reflects an accurate total after the channel list has been queried at least once. To show a total unread badge before the chat list screen appears, fetch the channel list first (for example, run a channel query in the background) so the SDK has the data to calculate from, then observe the total unread count. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unsubscribe = ChannelRepository.getTotalChannelsUnread( ({ data: unread, loading, error }) => { if (error) { handleError(error); return; } if (!loading && unread) { renderResults({ unreadCount: unread.unreadCount, isMentioned: unread.isMentioned, }); } }, ); unsubscribe(); ``` ```swift iOS var cancellables = Set() channelRepository.getTotalChannelsUnread() .sink { unread in showSuccessMessage([ "unreadCount": unread.unreadCount, "isMentioned": unread.isMentioned ]) } .store(in: &cancellables) ``` ```kotlin Android val disposable = channelRepository .getTotalChannelUnread() .subscribe( { unread -> showSuccessMessage( mapOf( "unreadCount" to unread.unreadCount, "isMentioned" to unread.isMentioned, ), ) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter AmityChatClient.newChannelRepository() .getChannelTotalUnreads() .listen((unread) { final totalUnreadCount = unread.unreadCount; final hasMention = unread.isMentioned; }); ``` ## Implementation Notes Per-channel values come from channel or subchannel models, so update the UI from SDK observations instead of maintaining a separate unread counter. Total channel unread is derived from locally synced channels. Fetch the channel list at least once before the count reflects an accurate total. Use `isMentioned` to visually prioritize channels where the current user has unread mentions. When your app uses subchannels directly, read subchannel unread fields from the subchannel model where the platform exposes them. Mark messages as read from the message model to update unread state. ## Related Topics Mark messages as read. Subscribe to receipt topics while a chat screen is open. Show latest-message previews beside unread badges. --- ### [Message Read Status](https://learn.social.plus/social-plus-sdk/chat/engagement-features/unread-status/message-read-status) > Mark chat messages as read and inspect read-count fields from SDK message models. Use message read status when a user views a message and your app needs unread counts to move forward. The SDK exposes `markRead()` on message models across TypeScript, iOS, Android, and Flutter. ## Platform Surface | Surface | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Mark read | `message.markRead()` | `message.markRead()` | `message.markRead()` | `message.markRead()` | | Read count | `message.readCount` | `message.readCount` | `message.getReadCount()` | `message.readCount` | | Delivered count | `message.deliveredCount` | `message.deliveredCount` | `message.getDeliveredCount()` | `message.deliveredCount` | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `message` | Yes | Message model returned by `getMessage` or a message query. | | `messageId` | Yes when fetching first | Message ID used to retrieve a message before marking it read. | | `subChannelId` | Indirect | The SDK reads this from the message model where needed. | | `channelSegment` / segment | Indirect | The SDK reads this from the message model to sync the read position. | ## Mark Message Read Call `markRead()` after your app decides the message has been seen, such as when the message becomes visible in an active chat screen. ```typescript TypeScript message.markRead(); renderResults({ messageId: message.messageId, readCount: message.readCount, }); ``` ```swift iOS let liveMessage = messageRepository.getMessage("message-id") token = liveMessage.observe { liveObject, _ in guard let currentMessage = liveObject.snapshot else { return } currentMessage.markRead() showSuccessMessage(currentMessage.readCount) } ``` ```kotlin Android val currentMessage = message ?: return currentMessage.markRead() showSuccessMessage(currentMessage.getReadCount()) ``` ```dart Flutter final fetchedMessage = await AmityChatClient.newMessageRepository() .getMessage(messageId); fetchedMessage.markRead(); final readCount = fetchedMessage.readCount ?? 0; ``` ## Inspect Receipt Counts Read-count and delivered-count fields are available on message models. Use user-list queries from [Message Delivery Status](./message-delivery-status) when you need the user identities behind the counts. ```typescript TypeScript renderResults({ readCount: message.readCount, deliveredCount: message.deliveredCount, }); ``` ```swift iOS let liveMessage = messageRepository.getMessage("message-id") token = liveMessage.observe { liveObject, _ in guard let currentMessage = liveObject.snapshot else { return } showSuccessMessage([ "readCount": currentMessage.readCount, "deliveredCount": currentMessage.deliveredCount ]) } ``` ```kotlin Android val currentMessage = message ?: return showSuccessMessage( mapOf( "readCount" to currentMessage.getReadCount(), "deliveredCount" to currentMessage.getDeliveredCount(), ), ) ``` ```dart Flutter final fetchedMessage = await AmityChatClient.newMessageRepository() .getMessage(messageId); final readCount = fetchedMessage.readCount ?? 0; final deliveredCount = fetchedMessage.deliveredCount ?? 0; ``` ## Implementation Notes `markRead()` is a method on the message object, so load or query messages before marking them read. Marking messages read is the write path that lets unread count state advance for the current user. Your app decides when a message is considered visible enough to mark as read. Use receipt-user APIs where supported when you need identities, not just count fields. ## Related Topics Read per-channel and total unread counts. Mark delivered and query receipt users where supported. Keep receipt state current while a chat screen is open. --- ### [Message Delivery Status](https://learn.social.plus/social-plus-sdk/chat/engagement-features/unread-status/message-delivery-status) > Mark chat messages as delivered and query read or delivered users where the SDK supports it. Use message delivery status when your app needs delivered receipts, read-user lists, or delivered-user lists for a message. TypeScript, iOS, and Android expose explicit delivery and receipt-user APIs. Flutter currently exposes message count fields, but not public APIs for marking a message delivered or querying receipt users. ## Platform Surface | Surface | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Mark delivered | `MessageRepository.markAsDelivered(subChannelId, messageId)` | `message.markAsDelivered()` | `message.markAsDelivered()` | Not exposed | | Get read users | `MessageRepository.getReadUsers(query)` | `message.getReadUsers(memberships:)` | `message.getReadUsers(memberships)` | Not exposed | | Get delivered users | `MessageRepository.getDeliveredUsers(query)` | `message.getDeliveredUsers(memberships:)` | `message.getDeliveredUsers(memberships)` | Not exposed | | Count fields | `message.readCount`, `message.deliveredCount` | `message.readCount`, `message.deliveredCount` | `message.getReadCount()`, `message.getDeliveredCount()` | `message.readCount`, `message.deliveredCount` | Flutter developers can read `readCount` and `deliveredCount` from `AmityMessage`, but this SDK checkout does not expose public delivered-user or read-user query methods. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `subChannelId` | Yes for TypeScript delivery marking | Subchannel ID that owns the message being marked as delivered. | | `messageId` | Yes | Message ID to mark delivered or use for receipt-user queries. | | `message` | Yes for iOS and Android model methods | Message model returned by the SDK. | | `memberships` | No | Filters receipt users by membership state. Supported values are member, banned, muted, non-member, and deleted. | | Pagination controls | No | TypeScript returns a paged response; Android returns `PagingData`; iOS returns an `AmityCollection`. | ## Mark Message Delivered Call the delivered API when a received message has reached the recipient device and your app needs to sync that delivery state. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const didMarkDelivered = await MessageRepository.markAsDelivered( subChannelId, messageId, ); if (didMarkDelivered) { showSuccessMessage(messageId); } ``` ```swift iOS let liveMessage = messageRepository.getMessage("message-id") token = liveMessage.observe { liveObject, _ in guard let currentMessage = liveObject.snapshot else { return } Task { do { try await currentMessage.markAsDelivered() showSuccessMessage(currentMessage.messageId) } catch { handleError(error) } } } ``` ```kotlin Android val currentMessage = message ?: return val disposable = currentMessage .markAsDelivered() .subscribe( { showSuccessMessage(currentMessage.getMessageId()) }, { error -> handleGeneralError(error) }, ) ``` ## Query Read Users Use read-user queries when you need to show who has read a message. Apply membership filters when your product only wants active members or needs to include muted, banned, non-member, or deleted users. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: users, nextPage } = await MessageRepository.getReadUsers({ messageId, memberships: ['member', 'muted', 'non-member'], }); renderResults({ users, nextPage, }); ``` ```swift iOS let liveMessage = messageRepository.getMessage("message-id") token = liveMessage.observe { liveObject, _ in guard let currentMessage = liveObject.snapshot else { return } let memberships: Set = [ .member, .muted, .nonMember ] let readUsers = currentMessage.getReadUsers(memberships: memberships) showSuccessMessage(readUsers) } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.chat.message.MessageReadMembershipFilter val currentMessage = message ?: return val disposable = currentMessage .getReadUsers( memberships = listOf( MessageReadMembershipFilter.MEMBER, MessageReadMembershipFilter.MUTED, MessageReadMembershipFilter.NOT_MEMBER, ), ) .subscribe( { pagingData -> getPagingData(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ## Query Delivered Users Use delivered-user queries when your sender UI or moderation tooling needs to know which recipients have received a message. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const { data: users, nextPage } = await MessageRepository.getDeliveredUsers({ messageId, memberships: ['member', 'banned', 'deleted'], }); renderResults({ users, nextPage, }); ``` ```swift iOS let liveMessage = messageRepository.getMessage("message-id") token = liveMessage.observe { liveObject, _ in guard let currentMessage = liveObject.snapshot else { return } let memberships: Set = [ .member, .banned, .deleted ] let deliveredUsers = currentMessage.getDeliveredUsers(memberships: memberships) showSuccessMessage(deliveredUsers) } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.chat.message.MessageDeliveredMembershipFilter val currentMessage = message ?: return val disposable = currentMessage .getDeliveredUsers( memberships = listOf( MessageDeliveredMembershipFilter.MEMBER, MessageDeliveredMembershipFilter.BANNED, MessageDeliveredMembershipFilter.DELETED, ), ) .subscribe( { pagingData -> getPagingData(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ## Receipt User Filters | Logical value | TypeScript | iOS | Android | | --- | --- | --- | --- | | Member | `'member'` | `.member` | `MEMBER` | | Banned | `'banned'` | `.banned` | `BANNED` | | Muted | `'muted'` | `.muted` | `MUTED` | | Non-member | `'non-member'` | `.nonMember` | `NOT_MEMBER` | | Deleted | `'deleted'` | `.deleted` | `DELETED` | ## Related Topics Mark messages as read and inspect read count fields. Start receipt sync while a chat screen is active. Load the message models used by receipt APIs. --- ### [Message Receipt Sync](https://learn.social.plus/social-plus-sdk/chat/engagement-features/unread-status/message-receipt-sync) > Start and stop chat message receipt synchronization for an active subchannel. Use message receipt sync while a user is viewing a chat screen. It subscribes the SDK to receipt updates for a subchannel so read and delivery state can stay current. Stop sync when the user leaves the screen. This page covers the explicit public receipt-sync APIs. Flutter does not expose `startMessageReceiptSync` or `stopMessageReceiptSync` in the current public SDK surface. ## Platform Surface | Operation | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Start sync | `SubChannelRepository.startMessageReceiptSync(subChannelId)` | `AmitySubChannelRepository().startMessageReceiptSync(subChannelId:)` | `subChannelRepository.startMessageReceiptSync(subChannelId)` | Not exposed | | Stop sync | `SubChannelRepository.stopMessageReceiptSync(subChannelId)` | `AmitySubChannelRepository().stopMessageReceiptSync(subChannelId:)` | `subChannelRepository.stopMessageReceiptSync(subChannelId)` | Not exposed | | Result | `Promise` / `boolean` | `Void` | `Completable` | Not exposed | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `subChannelId` | Yes | Subchannel whose message receipt topic should be synchronized. | | Active chat lifecycle | Yes | Start sync when a user enters the message view and stop it when they leave. | | Channel object | No | If you only have a channel, use its default subchannel ID where the platform exposes it. | | Cleanup handle | Depends | TypeScript stop is explicit; Android and iOS use explicit stop calls; keep your own screen lifecycle ownership. | ## Start Receipt Sync Start sync before or while rendering the active message list for a subchannel. ```typescript TypeScript import { SubChannelRepository } from '@amityco/ts-sdk'; const didStart = await SubChannelRepository.startMessageReceiptSync( subChannelId, ); if (didStart) { showSuccessMessage(subChannelId); } ``` ```swift iOS let subChannelRepository = AmitySubChannelRepository() try await subChannelRepository.startMessageReceiptSync( subChannelId: "sub-channel-id" ) showSuccessMessage("sub-channel-id") ``` ```kotlin Android val subChannelRepository = AmityChatClient.newSubChannelRepository() val disposable = subChannelRepository .startMessageReceiptSync(subChannelId) .subscribe( { showSuccessMessage(subChannelId) }, { error -> handleGeneralError(error) }, ) ``` ## Stop Receipt Sync Stop sync when the active chat view closes, changes to another subchannel, or no longer needs live receipt state. ```typescript TypeScript import { SubChannelRepository } from '@amityco/ts-sdk'; const didStop = SubChannelRepository.stopMessageReceiptSync(subChannelId); if (didStop) { showSuccessMessage(subChannelId); } ``` ```swift iOS let subChannelRepository = AmitySubChannelRepository() try await subChannelRepository.stopMessageReceiptSync( subChannelId: "sub-channel-id" ) showSuccessMessage("sub-channel-id") ``` ```kotlin Android val subChannelRepository = AmityChatClient.newSubChannelRepository() val disposable = subChannelRepository .stopMessageReceiptSync(subChannelId) .subscribe( { showSuccessMessage(subChannelId) }, { error -> handleGeneralError(error) }, ) ``` ## Implementation Notes Tie receipt sync to the active chat screen, not to app startup. The public APIs take a `subChannelId`, so resolve the default subchannel from the channel when needed. Receipt sync keeps receipt state fresh; use `message.markRead()` to mark messages read. Do not add Flutter start/stop examples unless the Flutter SDK exposes public APIs for them. ## Related Topics Mark messages as read. Mark delivered and query receipt users. Show unread count and mention state in channel lists. ## Social — Communities ### [Social Module](https://learn.social.plus/social-plus-sdk/social/README) > Add posts, comments, communities, reactions, notifications, and feeds to your app using the social.plus SDK Social Module. Use the social.plus SDK Social Module to add posts, comments, communities, reactions, user relationships, notifications, stories, and feeds to your app. The SDK provides client APIs for custom UI implementations on top of social.plus managed infrastructure. Posts, comments, stories, and rich media content with real-time updates Create and manage communities with membership, roles, and moderation tools Reactions, notifications, feeds, search, and social interactions ## Key Capabilities **Posts, comments, stories, and media** - **Post Types**: Text, images, videos, files, polls, live stream posts, room posts, and custom post types - **Comments**: Create, query, edit, delete, and moderate comment threads - **Stories**: Create, retrieve, delete, and inspect story impressions where stories are enabled - **Media Handling**: Upload and display files, images, and videos through the SDK media guides - **Reactions & Mentions**: Add engagement and user references to supported content **Community structure, membership, and moderation** - **Community Creation**: Public or private communities with descriptions, avatars, tags, categories, and metadata - **Member Management**: Join, leave, add, remove, query, ban, mute, and role workflows - **Moderation**: Post review settings, content flagging, reporting, and permission checks - **Community Discovery**: Query, keyword search, category filtering, tags, trending, and recommended community collections where supported - **Governance**: Role-based permissions and community-level posting controls **Relationships and safety controls** - **Following**: Follow, unfollow, accept or decline follow requests, and query follower/following lists - **Blocking**: Block, unblock, and manage blocked users - **User Profile Fields**: Query and update social profile fields documented in User Management - **Relationship Status**: Read connection state and request state where supported **Feeds, search, and browsing** - **Feeds**: Query user, community, global, and For You (personalized) feeds - **Global Feed Ranking**: Use chronological ordering or configured custom post ranking where enabled - **Community Search**: Query communities by keyword, category, tags, membership filter, and sort order - **Enhanced Search**: Intelligent Search has separate setup and activation requirements in the Discovery & Engagement docs **Notifications and realtime-aware interactions** - **Notification Tray**: Query notification items and status where available - **Notification Events**: Work with social notification event references and settings - **Push Notifications**: Register devices and configure user, channel, or community settings in the Realtime Communication docs - **Realtime Updates**: Use live objects, live collections, and realtime events where supported by the target SDK ## Core Features Create content with media and moderation support - **Post Creation**: Text, image, video, file, poll, live stream, room, and custom post guides - **Content Metadata**: Multiple media types, mentions, and custom data where supported - **Content Management**: Edit, delete, and moderate posts - **Analytics**: Post impression guides where available Enable rich discussions with threaded conversations - **Threaded Discussions**: Nested comments with parent-children reply chain - **Real-time Updates**: Live comment feeds and notifications - **Mentions & Reactions**: Tag users and react to comments - **Moderation Tools**: Flag inappropriate content and manage discussions Share ephemeral content with time-limited visibility - **Media Stories**: Create image and video stories - **Engagement Tracking**: View counts, impressions, and viewer lists - **Global & Targeted Feeds**: Distribute to supported story targets - **Story Management**: Create, retrieve, inspect, and delete stories - **Analytics**: Story performance and audience insights Express emotions and drive engagement through reactions - **Flexible Reactions**: Custom reaction types - **Reaction Analytics**: Track popular reactions - **Bulk Operations**: Add/remove reactions efficiently - **Social Proof**: Display reaction counts and user lists Build and manage thriving social communities - **Community Creation**: Public, and private communities - **Member Management**: Roles, permissions, and member management - **Moderation**: Post review, reporting, ban, mute, and role workflows - **Community Discovery**: Query, search, trending, recommended, category, and tag-based flows where supported Manage follows, blocks, and social graphs - **Follow/Unfollow System**: Social connections with status tracking - **Block/Unblock Users**: Privacy controls and content filtering - **Relationship Management**: Connection requests and social graphs - **Privacy Controls**: Manage visibility and interactions Feed and timeline query APIs - **Feed Types**: Global, For You (personalized), user, and community feeds - **Ranking**: Chronological ordering and configured custom post ranking - **Content Filtering**: Hide unwanted content and filter by tags, and keywords - **Performance Optimization**: Efficient pagination and caching strategies Search and enhanced discovery options - **Community Search**: Search and filter community collections - **Post Search**: Search posts where the search feature is enabled - **Activation**: Intelligent Search requires network-level activation - **Scoring**: Relevance scores are documented on the Intelligent Search pages Notification docs for social engagement - **Notification Tray**: Centralized notification management with read/unread states - **Event-based Notifications**: Posts, comments, reactions, mentions, and community activities - **Settings**: User, channel, and community notification settings where supported ## Getting Started Workflows **Essential Features**: Posts + Comments + Reactions + Feed **Perfect for**: Social media apps, news platforms, community forums **Implementation Path**: 1. **Setup Content Creation**: Implement post creation with [Posts](/social-plus-sdk/social/content-management/posts/overview) 2. **Enable Discussions**: Add comment functionality with [Comments](/social-plus-sdk/social/content-management/comments/overview) 3. **Add Engagement**: Implement reactions with [Reactions](/social-plus-sdk/core-concepts/content-handling/reactions) 4. **Create Feeds**: Query user, community, global, and For You (personalized) feeds with [Feed](/social-plus-sdk/social/discovery-engagement/feed/overview) 5. **Enable Notifications**: Setup [Notification System](/social-plus-sdk/social/discovery-engagement/notifications/overview) **Optional Enhancements**: Stories, user following, and search **Checkpoint**: Verify post, comment, reaction, feed, and notification behavior on the target SDKs you ship **Essential Features**: Communities + Posts + Moderation + Search **Perfect for**: Forums, professional networks, interest-based communities **Implementation Path**: 1. **Setup Communities**: Create community structure with [Communities](/social-plus-sdk/social/communities-spaces/overview) 2. **Enable Content Creation**: Add posts and discussions with moderation 3. **Implement User Management**: Setup roles and permissions 4. **Add Discovery**: Enable community search, feeds, and [Intelligent Search](/social-plus-sdk/social/discovery-engagement/search/overview) only where configured 5. **Setup Moderation**: Configure content filtering and community management **Optional Enhancements**: Stories, notification settings, and analytics/moderation Console workflows **Checkpoint**: Verify community creation, discovery, membership, roles, and moderation permissions on the target SDKs you ship **Essential Features**: User profiles + Follow system + Notifications **Perfect for**: Professional networks, dating apps, social discovery **Implementation Path**: 1. **Setup User Interactions**: Implement [Follow/Unfollow](/social-plus-sdk/social/user-relationship/following/follow-unfollow-user) 2. **Add Privacy Controls**: Enable [Block/Unblock](/social-plus-sdk/social/user-relationship/blocking/block-unblock-user) functionality 3. **Create Activity Feeds**: Build user-specific feeds and timelines 4. **Enable Notifications**: Setup [Notification Tray](/social-plus-sdk/social/discovery-engagement/notifications/overview) 5. **Add Discovery**: Add search and product-specific recommendation surfaces where needed **Optional Enhancements**: Messaging, notification settings, and product-specific recommendations **Checkpoint**: Verify follow request states, block behavior, user profile visibility, and notifications on the target SDKs you ship **Essential Features**: Selected social modules working together **Perfect for**: Products that combine feeds, communities, relationships, search, notifications, and moderation **Implementation Path**: 1. **Foundation**: Implement the core features your product needs (for example Posts, Comments, and Communities) 2. **Discovery**: Add [Search](/social-plus-sdk/social/discovery-engagement/search/overview), feeds, trending communities, and recommended communities where enabled 3. **Feed Ranking**: Decide whether chronological or configured custom post ranking is right for the product 4. **Analytics**: Use SDK analytics pages and Console analytics where they apply 5. **Moderation**: Combine SDK reporting, roles, permissions, and Console moderation workflows **Optional Enhancements**: Live streaming, stories, notification settings, and analytics/moderation Console workflows **Checkpoint**: Treat this as a cross-module integration and validate each target platform separately ## Platform Support **iOS, Android, TypeScript, and Flutter** - Native iOS and Android SDKs - TypeScript/JavaScript SDK for web apps - Flutter SDK for cross-platform mobile apps **Use the target SDK docs** - Setup requirements differ by platform - Method names and enum availability can vary - Validate snippets against the SDK you ship **Use when your implementation crosses SDK boundaries** - UIKit for prebuilt UI components - API reference for server-side workflows - Console docs for moderation and analytics operations ## Related Documentation **Real-time messaging capabilities** Build chat features with channels, messages, and real-time communication **SDK fundamentals and architecture** Learn about authentication, sessions, and core SDK concepts **Live streaming and broadcasting** Add room-based broadcasting, live viewing, and playback flows **Pre-built UI components** Ready-to-use UI components for rapid development **Analytics and insights** Track engagement, user behavior, and community health **Content moderation tools** Advanced content moderation and community safety features --- ### [Overview](https://learn.social.plus/social-plus-sdk/social/communities-spaces/overview) > Create and manage communities with configurable privacy, post moderation, membership workflows, roles, and content controls. Use the community SDK APIs to create communities, manage membership and roles, configure posting rules, and build discovery flows. Communities support public and private access modes, discoverability settings, join approval where configured, role-based permissions, and post or story moderation settings. **Looking for a step-by-step walkthrough?** The [Community Platform](/use-cases/social/community-platform) guide walks you through building communities with membership, governance, and moderation end-to-end. ## Core Capabilities Query, search, sort, and filter communities by membership, category, tags, and keyword Join, leave, add, remove, query, ban, mute, and role workflows Post settings, role-based permissions, reporting, and moderation actions Community categories, metadata, tags, counts, and status fields Community objects expose fields for identity, visibility, membership, moderation settings, tags, categories, counts, and status. Start with the fields your product needs, then add governance options as your community flows become clearer. ## Community Architecture Communities in social.plus group posts, members, roles, and moderation settings under a single community object. Use communities for forums, interest groups, private spaces, announcement areas, and other social areas that need membership and content controls. ### Core Components **Visual Identity** - Display name and description - Avatar and visual branding - Official verification status - Custom metadata fields **Discoverability** - Public/private visibility settings - Search tags and categorization - Query, search, trending, and recommended collections where supported - Category-based organization **Access Control** - Public vs private community types - Join approval requirements - Invitation workflows where configured - Membership state and access behavior **Membership Lifecycle** - User-initiated joining and leaving - Administrative member addition/removal - Role-based access and permissions - Member onboarding workflows **Content Management** - Post creation and moderation - Admin-only posting restrictions - Content approval workflows - Flagging and reporting systems **Governance Structure** - Role hierarchy and permissions - Moderation tools and enforcement - Community-level posting controls - Admin and moderator workflows **Community Counts** - Member count - Post count - Joined and deleted state - Flagged post/comment indicators **Operational Data** - Creation and modification timestamps - Deletion status - Official status - Associated channel ID ## Community Types & Access Models social.plus supports community configurations for open discovery, controlled membership, and official community labeling: **Open Access**: Can be discoverable and joinable by users **Moderated Access**: Can require approval to join when configured **Content Visibility**: Build product-specific visibility behavior from community and post query results **Controlled Access**: Intended for restricted membership flows **Discovery**: Usually hidden from general discovery depending on configuration **Membership**: Use invitation, add member, or approval flows that match your product **Official Flag**: Community objects can expose official status **Product Display**: Use this flag for badges or filtering if your product needs it **Administration**: Treat official designation as an admin-controlled field ## Common Community Fields Community objects include identity, visibility, membership, moderation, organization, and status fields. Field availability and naming can vary slightly across SDKs, so use the model exposed by your target platform SDK. ### Core Properties | Property | Type | Description | |----------|------|-------------| | `communityId` | String | Unique identifier for the community | | `channelId` | String | Associated channel identifier | | `userId` | String | ID of the user who created the community | | `displayName` | String | Community name for displaying | | `description` | String | Description of the community | | `avatar` | Object | Avatar object for community branding | ### Access & Visibility Controls | Property | Type | Description | |----------|------|-------------| | `isPublic` | Boolean | Is this community public? | | `isDiscoverable` | Boolean | Whether this community can appear in discovery/search surfaces | | `requiresJoinApproval` | Boolean | Whether joining this community requires approval | | `isOfficial` | Boolean | Is this community official? | ### Content & Moderation | Property | Type | Description | |----------|------|-------------| | `onlyAdminCanPost` | Boolean | Only admins can post in this community | | `postSettings` | Object | Community post settings such as review-required or admin-only posting | | `hasFlaggedPost` | Boolean | Indicates whether the community has flagged posts | | `hasFlaggedComment` | Boolean | Indicates whether the community has flagged comments | ### Organization & Discovery | Property | Type | Description | |----------|------|-------------| | `categoryIds` | List | IDs of categories associated with the community | | `tags` | List | Tags used for filtering and search | | `metadata` | Object | Custom fields for your product. Do not store sensitive personal data here. | ### Counts & Status | Property | Type | Description | |----------|------|-------------| | `postsCount` | Integer | Number of posts in the community | | `membersCount` | Integer | Number of members in the community | | `isJoined` | Boolean | Is this community joined? | | `isDeleted` | Boolean | Is this community deleted? | | `createdAt` | DateTime | Date/time when the community was created | | `updatedAt` | DateTime | Date/time when a community is updated or deleted | For create and update flows, review the platform-specific community builder or options object. Some fields are read-only response fields, while others can be configured when creating or editing a community. ## Best Practices **Start Simple, Then Add Governance**: Begin with community creation, query, join/leave, and basic moderation. Add categories, roles, approval, and notification settings as the product flow requires them. **Planning & Purpose** 1. **Clear Purpose**: Define community goals and target audience before creation 2. **Appropriate Access Model**: Choose public/private settings based on community needs 3. **Governance Planning**: Establish moderation policies and role structures early 4. **Growth Strategy**: Plan discovery, onboarding, and member retention flows **Design Considerations** - Start with basic features and expand gradually - Consider your target audience's needs and behaviors - Keep privacy, moderation, and discovery decisions explicit - Design clear user journeys for joining and participating **Architecture & Development** 1. **Data Modeling**: Understand the community object structure before building 2. **Permission Architecture**: Implement proper role-based access controls from the start 3. **User Experience**: Design intuitive flows for joining, participating, and moderating 4. **Performance Optimization**: Plan for pagination and live collection updates **Development Best Practices** - Implement proper error handling for community operations - Use appropriate data validation for community properties - Design for mobile-first community experiences - Subscribe to live objects or live collections where supported by the target SDK **Operational Excellence** 1. **Active Moderation**: Implement consistent content oversight and member management 2. **Clear Guidelines**: Establish and communicate community rules and expectations 3. **Member Engagement**: Foster participation through events, discussions, and recognition 4. **Continuous Improvement**: Use available counts, moderation state, and your product analytics to iterate on community features **Management Strategies** - Establish clear moderation workflows and escalation paths - Create onboarding experiences for new community members - Implement feedback loops to understand member satisfaction - Regularly review moderation queues and community health signals **Sustainable Growth** - Focus on quality over quantity in early growth phases - Implement referral and invitation systems for organic growth - Use query, search, category, tag, trending, or recommended community flows where they fit the product - Create incentives for active community participation **Scaling Considerations** - Plan moderation resources as communities grow - Use SDK moderation actions and Console moderation workflows where appropriate - Consider community subdivision strategies for large groups - Monitor product metrics as membership increases ## Next Steps Ready to start building? Choose your implementation path: Step-by-step community creation guide Build community search and browsing Configure roles, permissions, and moderation **Need Help?** Start with the creation, query, membership, and moderation guides below, then add categories, roles, and notification settings as your use case grows. --- ### [Create Community](https://learn.social.plus/social-plus-sdk/social/communities-spaces/community-lifecycle/create-community) > Create a community with SDK-backed privacy, moderation, story, category, metadata, and member settings. Use the create-community API for your platform to set up a community with a display name, description, privacy mode, category IDs, post moderation setting, story settings, metadata, and optional initial members. Public/private visibility, discoverability, and join approval are separate settings in the SDK. Configure the combination that matches your product flow, then rely on backend permission enforcement for actual access control. ## Parameters TypeScript exposes `CommunityRepository.createCommunity()`. iOS uses `AmityCommunityCreateOptions` with `createCommunity(with:)`. Android and Flutter start from `AmitySocialClient.newCommunityRepository().createCommunity(...)`. | Setting | Platforms | Description | |---------|-----------|-------------| | `displayName` | TypeScript, iOS, Android, Flutter | Required community display name | | `description` | TypeScript, iOS, Android, Flutter | Optional community description | | `isPublic` | TypeScript, iOS, Android, Flutter | Public/private visibility flag | | `avatarFileId` / `avatar` | TypeScript / iOS, Android, Flutter | Avatar file ID or uploaded SDK image object | | `categoryIds` | TypeScript, iOS, Android, Flutter | Category IDs linked to the community | | `postSetting` / `postSettings` | TypeScript, iOS, Android, Flutter | Post creation and review setting | | `storySetting` / `storySettings` | TypeScript, iOS, Android, Flutter | Story comment setting | | `metadata` | TypeScript, iOS, Android, Flutter | Custom metadata object | | `userIds` | TypeScript, iOS, Android, Flutter | Initial members to add during creation | | `tags` | TypeScript, Android, Flutter | Search/filter tags | | `isDiscoverable` | TypeScript, iOS, Android | Whether a private community can appear in discovery surfaces | | `requiresJoinApproval` | TypeScript, iOS, Android | Whether join requests require approval | ## Privacy Settings | Setting | Description | |---------|-------------| | `isPublic` | Controls whether the community is public or private | | `isDiscoverable` | Lets supported SDKs create discoverable private communities | | `requiresJoinApproval` | Lets supported SDKs request approval before users join | ## Post Moderation Options | Concept | TypeScript | iOS | Android / Flutter | |---------|------------|-----|-------------------| | Anyone can post | `ANYONE_CAN_POST` | `.anyoneCanPost` | `ANYONE_CAN_POST` | | Admin review required | `ADMIN_REVIEW_POST_REQUIRED` | `.adminReviewPostRequired` | `ADMIN_REVIEW_POST_REQUIRED` | | Admins only | `ONLY_ADMIN_CAN_POST` | `.onlyAdminCanPost` | `ADMIN_CAN_POST_ONLY` | ## Story Settings Communities can be configured with story comment settings: | Platform | Shape | |----------|-------| | TypeScript | `storySetting: { enableComment: true }` | | iOS | `setStorySettings(allowComment: true)` | | Android / Flutter | `AmityCommunityStorySettings(allowComment = true)` / `AmityCommunityStorySettings(allowComment: true)` | ## Create a Community Use this method after your app has collected the required display name and any optional privacy, moderation, category, member, tag, or metadata settings. ```typescript TypeScript import { CommunityPostSettings, CommunityRepository } from '@amityco/ts-sdk'; async function createCommunity(): Promise { const { data: community } = await CommunityRepository.createCommunity({ displayName: 'My Community', description: 'Community description', isPublic: true, isDiscoverable: true, requiresJoinApproval: false, postSetting: CommunityPostSettings.ANYONE_CAN_POST, storySetting: { enableComment: true }, categoryIds: ['categoryId'], tags: ['product'], metadata: { topic: 'product' }, userIds: ['userId1', 'userId2'], }); return community; } ``` ```swift iOS let options = AmityCommunityCreateOptions() options.setDisplayName("My Community") options.setCommunityDescription("Community description") options.setIsPublic(true) options.setCategoryIds(["categoryId"]) options.setUserIds(["userId1", "userId2"]) options.setPostSettings(.anyoneCanPost) options.setStorySettings(allowComment: true) options.setIsDiscoverable(true) options.setRequiresJoinApproval(false) options.setMetadata(["topic": "product"]) let community = try await communityRepository.createCommunity(with: options) ``` ```kotlin Android fun createCommunity() { val metadata = JsonObject().apply { addProperty("topic", "product") } AmitySocialClient.newCommunityRepository() .createCommunity( displayName = "My Community", isDiscoverable = true, requiresJoinApproval = false ) .description("Community description") .isPublic(true) .categoryIds(listOf("categoryId")) .userIds(listOf("userId1", "userId2")) .tags(listOf("product")) .metadata(metadata) .postSettings(AmityCommunityPostSettings.ANYONE_CAN_POST) .storySettings(AmityCommunityStorySettings(allowComment = true)) .build() .create() .doOnSuccess { community: AmityCommunity -> // Community created. } .doOnError { error -> // Handle error. } .subscribe() } ``` ```dart Flutter void createCommunity() { AmitySocialClient.newCommunityRepository() .createCommunity('My Community') .description('Community description') .categoryIds(['categoryId']) .userIds(['userId1', 'userId2']) .tags(['product']) .isPublic(true) .metadata({'topic': 'product'}) .postSetting(AmityCommunityPostSettings.ANYONE_CAN_POST) .storySettings(AmityCommunityStorySettings(allowComment: true)) .create() .then((AmityCommunity community) { // Community created. }) .onError((error, stackTrace) { // Handle error. }); } ``` Community tags are shown for TypeScript, Android, and Flutter. The current iOS create/update options do not expose a public community tag setter. ## Best Practices Start with basic settings and allow community owners to customize moderation and features after creation to avoid overwhelming the initial creation flow. ### Creation Flow Guidelines 1. Show essential settings first, advanced options later. 2. Use sensible defaults to reduce decision fatigue. 3. Allow users to preview community settings before creation. 4. Guide new community creators through setup. ### Performance Optimization 1. Compress avatar images before upload. 2. Validate metadata structure client-side. 3. Handle creation asynchronously with loading states. 4. Provide clear error messages and retry options. ## Related Topics Modify community settings and properties after creation Organize communities with category management Handle membership and moderation after community creation Make your created communities discoverable to users --- ### [Update Community](https://learn.social.plus/social-plus-sdk/social/communities-spaces/community-lifecycle/update-community) > Update community profile, visibility, categories, moderation, story, and metadata settings with the SDK. Use the update API for your platform to change a community's display name, description, avatar, category IDs, privacy mode, post moderation setting, story settings, or custom metadata. TypeScript and Flutter use `updateCommunity()`, while iOS and Android use `editCommunity`. The backend enforces who can update a community. In your UI, expose these controls only to users who can manage community settings, such as creators, moderators, or administrators in your product model. ## Parameters Community updates modify the community record while preserving the same `communityId`. Use them for profile changes, visibility changes, category changes, and moderation-setting changes. | Setting | Platforms | Description | |---------|-----------|-------------| | `communityId` | TypeScript, iOS, Android, Flutter | Required community ID | | `displayName` | TypeScript, iOS, Android, Flutter | Updated display name | | `description` | TypeScript, iOS, Android, Flutter | Updated description | | `isPublic` | TypeScript, iOS, Android, Flutter | Updated public/private visibility | | `avatarFileId` / `avatar` | TypeScript / iOS, Android, Flutter | Updated avatar file ID or uploaded SDK image object | | `categoryIds` | TypeScript, iOS, Android, Flutter | Updated category IDs | | `postSetting` / `postSettings` | TypeScript, iOS, Android, Flutter | Updated post creation and review setting | | `storySetting` / `storySettings` | TypeScript, iOS, Android, Flutter | Updated story comment setting | | `metadata` | TypeScript, iOS, Android, Flutter | Updated custom metadata object | | `tags` | TypeScript, Android, Flutter | Updated search/filter tags | | `isDiscoverable` | TypeScript, iOS, Android | Updated discoverability setting | | `requiresJoinApproval` | TypeScript, iOS, Android | Updated join approval setting | ## Permission Requirements Do not treat client-side role checks as the source of truth. They are useful for hiding or showing controls, but the SDK call still depends on the permissions enforced by your social.plus backend configuration. ## Update a Community Use this method for profile, visibility, category, moderation, story, tag, and metadata changes after a community has already been created. ```typescript TypeScript import { CommunityPostSettings, CommunityRepository } from '@amityco/ts-sdk'; async function updateCommunity() { const updatedCommunity: Parameters[1] = { avatarFileId: 'fileId', description: 'Updated community description', displayName: 'Updated community name', isPublic: true, categoryIds: ['news'], tags: ['product'], metadata: { topic: 'product' }, postSetting: CommunityPostSettings.ADMIN_REVIEW_POST_REQUIRED, storySetting: { enableComment: true }, isDiscoverable: true, requiresJoinApproval: false, }; const { data: community } = await CommunityRepository.updateCommunity( 'communityId', updatedCommunity, ); return community; } ``` ```swift iOS let updateOptions = AmityCommunityUpdateOptions() updateOptions.setDisplayName("updated-name") updateOptions.setCommunityDescription("updated-description") updateOptions.setIsPublic(false) updateOptions.setCategoryIds(["categoryId"]) updateOptions.setPostSettings(.adminReviewPostRequired) updateOptions.setStorySettings(allowComment: true) updateOptions.setIsDiscoverable(true) updateOptions.setRequiresJoinApproval(false) updateOptions.setMetadata(["topic": "product"]) let community = try await communityRepository.editCommunity(withId: "community-id", options: updateOptions) ``` ```kotlin Android fun editCommunity() { val metadata = JsonObject().apply { addProperty("topic", "product") } AmitySocialClient.newCommunityRepository() .editCommunity( communityId = "communityId1", isDiscoverable = true, requiresJoinApproval = false ) .displayName("Updated community name") .isPublic(isPublic = true) .description(description = "Updated community description") .categoryIds(categoryIds = listOf("categoryId1", "categoryId2")) .tags(tags = listOf("product")) .metadata(metadata) .postSettings(AmityCommunityPostSettings.ADMIN_REVIEW_POST_REQUIRED) .storySettings(AmityCommunityStorySettings(allowComment = true)) .build() .apply() .doOnSuccess { community: AmityCommunity -> // Community updated. } .doOnError { error -> // Handle error. } .subscribe() } ``` ```dart Flutter void updateCommunity(String communityId, AmityImage updatingAvatar) { AmitySocialClient.newCommunityRepository() .updateCommunity(communityId) .avatar(updatingAvatar) .displayName('Updated community name') .description('Updated community description') .tags(['product']) .categoryIds(['categoryId1', 'categoryId2']) .isPublic(false) .postSetting(AmityCommunityPostSettings.ADMIN_REVIEW_POST_REQUIRED) .storySettings(AmityCommunityStorySettings(allowComment: true)) .metadata({'topic': 'product'}) .update() .then((AmityCommunity community) { // Community updated. }) .onError((error, stackTrace) { // Handle error. }); } ``` ## Privacy and Visibility Updates ### Public to Private Changes Changing a public community to private affects how users discover and join it: Changing visibility affects discovery and join flows. Check the resulting community state and update your UI accordingly. ### Private to Public Changes Changing a private community to public can affect discovery and new membership flows: Making a community public can make it available to a wider audience, depending on your discovery query and backend configuration. ## Best Practices Notify community members about significant updates, such as privacy changes or moderation policy updates, to maintain transparency and community trust. ### Update Guidelines 1. Make small, focused updates rather than large batch changes. 2. Inform members about policy or privacy changes. 3. Preserve important metadata when updating. 4. Validate changes before applying to prevent errors. 5. Test updates in staging environments when possible. ## Related Topics Learn about initial community creation and setup Delete a community and handle client-side cleanup Advanced moderation features and member management Organize communities with category management --- ### [Delete Community](https://learn.social.plus/social-plus-sdk/social/communities-spaces/community-lifecycle/delete-community) > Delete a community by ID with SDK permission enforcement and client-side cleanup. The SDK exposes a delete-community API for authorized users when a community is no longer needed. Treat deletion as irreversible from the client UI, and confirm your product's data-retention and recovery policy outside the client SDK. Deleting a community cannot be undone through the client SDK. Use explicit confirmation in your product UI before calling the SDK method. ## Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `communityId` | String | Yes | Unique identifier of the community to delete | ## Permission Requirements Deletion is permission-gated by the backend. In most products, deletion controls are shown only to community creators, moderators, or administrators, but the SDK call is still validated server-side. ## Delete a Community Call the delete-community method with a `communityId`. TypeScript, Android, and Flutter use `deleteCommunity`; iOS uses `deleteCommunity(withId:)`. ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; async function deleteCommunity(communityId: Amity.Community['communityId']): Promise { const deletedCommunity = await CommunityRepository.deleteCommunity(communityId); return deletedCommunity; } ``` ```swift iOS try await communityRepository.deleteCommunity(withId: "community-id") ``` ```kotlin Android fun deleteCommunity() { AmitySocialClient.newCommunityRepository() .deleteCommunity(communityId = "communityId") .doOnComplete { // Community deleted. } .doOnError { error -> // Handle error. } .subscribe() } ``` ```dart Flutter void deleteCommunity(String communityId) { AmitySocialClient.newCommunityRepository() .deleteCommunity(communityId) .then((value) { // Community deleted. }) .onError((error, stackTrace) { // Handle error. }); } ``` ## Client Impact After successful deletion, handle the client-side effects explicitly: - Remove the community from any local list or cached UI state. - Navigate away from deleted community detail screens. - Stop showing actions that require the deleted `communityId`. - Refresh affected discovery or membership queries when needed. ## Pre-deletion Considerations ### Product Policy Before exposing deletion, decide how your product handles: 1. What data is retained or recoverable outside the client SDK. 2. Whether members should be notified before deletion. 3. Whether archive, privacy, or moderation actions are safer than deletion. ### Member Management Refresh membership and discovery views after deletion so users do not see stale references to the deleted community. ## Best Practices Implement multiple confirmation steps and consider a cooling-off period for community deletion requests to prevent impulsive decisions. ### Implementation Guidelines 1. Require multiple explicit confirmations. 2. Make users type the community name to confirm. 3. Verify user permissions before showing delete options. 4. Show clear progress during deletion operations. 5. Provide clear confirmation of successful deletion. ### User Experience Considerations 1. Emphasize the permanent nature of deletion. 2. Offer data export before deletion if your product supports it. 3. Provide tools to notify community members. 4. Handle post-deletion navigation appropriately. ## Related Topics Modify community settings as an alternative to deletion Manage communities with moderation tools instead of deletion Learn about community creation to understand deletion impact Handle member relationships before community deletion --- ### [Query Communities](https://learn.social.plus/social-plus-sdk/social/communities-spaces/discovery/query-communities) > Query and search communities by membership, category, tags, keyword, and sort order. Use community query APIs to browse communities by membership, category, tag filters where supported, and sort order. Use the platform's search API or keyword option when you need name-based search. Query communities with filtering and sorting options Find specific communities using keyword-based search Filter by user membership status and community access Organize discovery by community categories and topics ## Parameters ### Query Parameters | Setting | Platforms | Description | | --- | --- | --- | | Membership filter | TypeScript, iOS, Android, Flutter | Filter by all, joined, or not-joined communities. | | Sort order | TypeScript, iOS, Android, Flutter | Sort by display name, newest first, or oldest first. | | `categoryId` | TypeScript, iOS, Android, Flutter | Filter by category ID. | | `tags` | TypeScript, Android, Flutter | Filter by community tags. | | `includeDeleted` | TypeScript, iOS, Android, Flutter | Include or exclude deleted communities. | | `includeDiscoverablePrivateCommunity` | TypeScript, iOS, Android | Include discoverable private communities where supported. | ### Search Parameters | Parameter | Platforms | Description | | --- | --- | --- | | Search keyword | TypeScript, iOS, Android, Flutter | Community display-name keyword. TypeScript uses `displayName`, iOS and Android use `keyword`, and Flutter uses `.withKeyword(...)`. | | Membership filter | TypeScript, iOS, Android, Flutter | Filter by membership status. | | Sort order | TypeScript, iOS, Android, Flutter | Sort by display name, newest first, or oldest first. | | `categoryId` | TypeScript, iOS, Android, Flutter | Filter search results by category ID. | | `tags` | TypeScript, Android, Flutter | Filter search results by community tags. | | `includeDeleted` | TypeScript, iOS, Android, Flutter | Include deleted communities. | | `includeDiscoverablePrivateCommunity` | TypeScript, iOS, Android | Include discoverable private communities where supported. | ## Filter Options ### Membership Status Filtering Control discovery based on the current user's membership status: | Concept | TypeScript | iOS | Android / Flutter | |---------|------------|-----|-------------------| | All communities | `membership: "all"` | `.all` | `ALL` | | Joined communities | `membership: "member"` | `.userIsMember` | `MEMBER` | | Not joined communities | `membership: "notMember"` | `.userIsNotMember` | `NOT_MEMBER` | ### Sorting Options Organize community results to match your app's discovery flow: | Concept | TypeScript | iOS | Android / Flutter | |---------|------------|-----|-------------------| | Display name ascending | `"displayName"` | `.displayName` | `DISPLAY_NAME` | | Newest first | `"lastCreated"` | `.lastCreated` | `LAST_CREATED` | | Oldest first | `"firstCreated"` | `.firstCreated` | `FIRST_CREATED` | ### Category Filtering Communities can be organized by categories to help users find relevant content faster. When a `categoryId` is specified, results are filtered to only include communities belonging to that category. ### Tag Filtering Communities can be tagged to describe their topics or purpose. TypeScript, Android, and Flutter expose a `tags` filter on the query/search builders shown below. The published iOS SDK used for this page does not expose a tag parameter on `AmityCommunityQueryOptions` or `AmityCommunitySearchOptions`. Combine tag filtering with category and membership filters for precise discovery experiences. ## Query Communities The query-community API returns a collection of communities that match the provided filters. TypeScript, iOS, and Flutter expose Live Collection style APIs. Android returns `Flowable>`. Android's `getCommunities().withKeyword(...)` is deprecated. Use `searchCommunities(keyword = ...)` on Android for keyword search. ```swift iOS var queryCommunitiesToken: AmityNotificationToken? func queryCommunities() { let queryOptions = AmityCommunityQueryOptions( filter: .all, sortBy: .lastCreated, categoryId: "categoryId", includeDeleted: false, includeDiscoverablePrivateCommunity: true ) let liveCollection = communityRepository.getCommunities(with: queryOptions) queryCommunitiesToken = liveCollection.observe { collection, error in if let error = error { print("Error querying communities: \(error)") return } for community in collection.snapshots { // Handle each community in the results print("Community: \(community.displayName)") } } } ``` ```kotlin Android fun queryCommunities() { AmitySocialClient.newCommunityRepository() .getCommunities(includeDiscoverablePrivateCommunity = true) .sortBy(sortBy = AmityCommunitySortOption.LAST_CREATED) .filter(filter = AmityCommunityFilter.ALL) .categoryId(categoryId = "categoryId") .tags(tags = listOf("gaming", "tech")) .includeDeleted(includeDeleted = false) .build() .query() .doOnNext { pagingData: PagingData -> // Handle community results } .doOnError { error -> // Handle error } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; let nextPageFn: (() => void) | undefined; let hasMore: boolean | undefined; const unsubscribeCommunities = CommunityRepository.getCommunities( { membership: 'all', sortBy: 'lastCreated', categoryId: 'category-id', includeDeleted: false, includeDiscoverablePrivateCommunity: true, tags: ['gaming', 'tech'], limit: 20, }, ({ data: communities, onNextPage, hasNextPage, loading, error }) => { if (error) { // Handle error. } if (loading) { // Show loading state. } if (communities) { // Render communities. } hasMore = hasNextPage; nextPageFn = onNextPage; }, ); function loadMore() { if (hasMore) { nextPageFn?.(); } } ``` ```dart Flutter void queryCommunities() { final liveCollection = AmitySocialClient.newCommunityRepository() .getCommunities() .filter(AmityCommunityFilter.ALL) .sortBy(AmityCommunitySortOption.LAST_CREATED) .categoryId('categoryId') .tags(['gaming', 'tech']) .includeDeleted(false) .getLiveCollection(pageSize: 20); liveCollection.getStreamController().stream.listen((communities) { // Render communities. }, onError: (error) { // Handle error. }); liveCollection.loadNext(); } ``` ## Search Communities Use search for name-based discovery. The parameter name differs by platform: TypeScript uses `displayName`, iOS uses `keyword`, Android passes `keyword` into `searchCommunities(...)`, and Flutter uses `.withKeyword(...)` on the query builder. ```swift iOS var searchCommunitiesToken: AmityNotificationToken? func searchCommunitiesExample() { let searchOptions = AmityCommunitySearchOptions( keyword: "gaming", filter: .all, sortBy: .displayName, categoryId: nil, includeDeleted: false, includeDiscoverablePrivateCommunity: true ) let liveCollection = communityRepository.searchCommunities(with: searchOptions) searchCommunitiesToken = liveCollection.observe { collection, error in for community in collection.snapshots { // For example, to handle each community in the list. } } } ``` ```kotlin Android fun searchCommunities() { AmitySocialClient.newCommunityRepository() .searchCommunities( keyword = "gaming", includeDiscoverablePrivateCommunity = true ) .sortBy(sortBy = AmityCommunitySortOption.DISPLAY_NAME) .filter(filter = AmityCommunityFilter.ALL) .categoryId(categoryId = "categoryId") .tags(tags = listOf("gaming")) .includeDeleted(includeDeleted = false) .build() .query() .doOnNext { communities: PagingData -> // PagingData } .doOnError { // Exception } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; const unsubscribeSearch = CommunityRepository.searchCommunities( { displayName: 'gaming', membership: 'notMember', sortBy: 'displayName', categoryId: 'category-id', includeDeleted: false, includeDiscoverablePrivateCommunity: true, tags: ['gaming'], limit: 20, }, ({ data: communities, loading, error }) => { if (error) { // Handle error. } if (loading) { // Show loading state. } if (communities) { // Render search results. } }, ); ``` ```dart Flutter void searchCommunities() { final liveCollection = AmitySocialClient.newCommunityRepository() .getCommunities() .withKeyword('gaming') .filter(AmityCommunityFilter.NOT_MEMBER) .sortBy(AmityCommunitySortOption.DISPLAY_NAME) .tags(['gaming']) .includeDeleted(false) .getLiveCollection(pageSize: 20); liveCollection.getStreamController().stream.listen((communities) { // Render search results. }, onError: (error) { // Handle error. }); liveCollection.loadNext(); } ``` ## Related Topics Retrieve detailed information about specific communities Discover popular and recommended communities Learn about organizing communities with categories Help users join discovered communities --- ### [Get Community](https://learn.social.plus/social-plus-sdk/social/communities-spaces/discovery/get-community) > Retrieve a community by ID and observe community updates with the SDK. Call the get-community API with a `communityId` to retrieve a community's profile fields, counts, membership status, and settings. TypeScript and iOS expose Live Object style APIs, Android exposes an Rx stream, and Flutter exposes both a deprecated one-shot future and a live stream. Access name, description, avatar, and basic community information View member counts, join status, and community accessibility Real-time synchronization of community changes and member activity Retrieve community data without joining Use the live/observable form for detail screens so the UI can react when the community changes. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Get community by ID | `communityId` | Yes | Unique identifier of the community to retrieve. | ## Get Community by ID The get-community API retrieves a community by ID. Use it for detail screens, join previews, and any workflow that needs the current community record before taking an action. ```swift iOS var token: AmityNotificationToken? func observeCommunity(communityId: String) { token = communityRepository.getCommunity(withId: communityId).observe { liveObject, error in if let error = error { print("Error retrieving community: \(error)") return } guard let community = liveObject.snapshot else { print("Community not found") return } print("Community ID: \(community.communityId)") print("Name: \(community.displayName)") } } ``` ```kotlin Android fun observeCommunity() { AmitySocialClient.newCommunityRepository() .getCommunity(communityId = "communityId") .doOnNext { community: AmityCommunity -> println("Community ID: ${community.getCommunityId()}") println("Name: ${community.getDisplayName()}") } .doOnError { error -> println("Error retrieving community: ${error.message}") } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; const unsubscriber = CommunityRepository.getCommunity( 'communityId', ({ data: community, loading, error }) => { if (error) { // Handle any errors that occur during retrieving data } if (loading) { // Handle the loading state, e.g., show a loading spinner } if (community) { // Process the data } }, ); ``` ```dart Flutter void observeCommunity(String communityId) { AmitySocialClient.newCommunityRepository() .live .getCommunity(communityId) .listen((AmityCommunity community) { // Render community. }, onError: (error) { // Handle error. }); } ``` ## Related Topics Discover communities through search and filtering Learn how to implement community membership actions Understand community organization and categorization Implement community recommendation features --- ### [Trending & Recommended Communities](https://learn.social.plus/social-plus-sdk/social/communities-spaces/discovery/trending-and-recommended-communities) > Fetch trending and recommended communities with the SDK discovery APIs. Use the SDK discovery APIs to fetch trending and recommended community lists. These methods call the social.plus backend discovery endpoints and return community collections or lists, depending on platform. Fetch communities from the trending endpoint Fetch communities from the recommended endpoint The ranking logic is owned by the backend service. The client SDK exposes methods to request and render the returned communities. ## Parameters | Operation | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | --- | | Trending communities | `includeDiscoverablePrivateCommunity` | No | TypeScript, iOS, Android | Include discoverable private communities where supported. | | Trending communities | `limit` | No | TypeScript | Limit the returned collection size. | | Recommended communities | `includeDiscoverablePrivateCommunity` | No | TypeScript, iOS, Android | Include discoverable private communities where supported. | | Recommended communities | `limit` | No | TypeScript | Limit the returned collection size. | ## Trending Communities The `getTrendingCommunities()` method fetches communities from the trending endpoint. TypeScript exposes a Live Collection with pagination options, iOS exposes a Live Collection, Android returns a `Flowable>`, and Flutter returns a `Future>`. ```swift iOS var token: AmityNotificationToken? func observeTrendingCommunities() { token = communityRepository.getTrendingCommunities( includeDiscoverablePrivateCommunity: true ).observe { collection, error in for community in collection.snapshots { // For example, to handle each community in the list. } } } ``` ```kotlin Android fun queryTrendingCommunities() { AmitySocialClient.newCommunityRepository() .getTrendingCommunities(includeDiscoverablePrivateCommunity = true) .doOnNext { communities: List -> // Render trending communities. } .doOnError { error -> // Handle error. } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; const unsubscriber = CommunityRepository.getTrendingCommunities( { limit: 5, includeDiscoverablePrivateCommunity: true }, ({ data: communities, loading, error }) => { if (error) { // Handle error. } if (loading) { // Show loading state. } if (communities) { // Render trending communities. } }, ); ``` ```dart Flutter void getTrendingCommunities() { AmitySocialClient.newCommunityRepository() .getTrendingCommunities() .then((List communities) { // Render trending communities. }) .onError((error, stackTrace) { // Handle error. }); } ``` ## Recommended Communities The `getRecommendedCommunities()` method fetches communities from the recommended endpoint. Use it when your discovery UI needs a backend-curated list instead of a filter-based query. ```swift iOS var token: AmityNotificationToken? func observeRecommendedCommunities() { token = communityRepository.getRecommendedCommunities( includeDiscoverablePrivateCommunity: true ).observe { collection, error in for community in collection.snapshots { // For example, to handle each community in the list. } } } ``` ```kotlin Android fun queryRecommendedCommunities() { AmitySocialClient.newCommunityRepository() .getRecommendedCommunities(includeDiscoverablePrivateCommunity = true) .doOnNext { communities: List -> // Render recommended communities. } .doOnError { error -> // Handle error. } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; const unsubscriber = CommunityRepository.getRecommendedCommunities( { limit: 5, includeDiscoverablePrivateCommunity: true }, ({ data: communities, loading, error }) => { if (error) { // Handle error. } if (loading) { // Show loading state. } if (communities) { // Render recommended communities. } }, ); ``` ```dart Flutter void getRecommendedCommunities() { AmitySocialClient.newCommunityRepository() .getRecommendedCommunities() .then((List communities) { // Render recommended communities. }) .onError((error, stackTrace) { // Handle error. }); } ``` ## Best Practices **Recommendation Refresh**: Refresh trending and recommended communities intentionally, such as when the user opens the discovery surface or pulls to refresh. ### User Experience Guidelines 1. **Loading States**: Show skeleton screens during discovery data fetching 2. **Empty States**: Provide fallback content when no recommendations are available 3. **Action Feedback**: Give immediate feedback when users join discovered communities ### Performance Optimization 1. **Pagination**: Use TypeScript pagination options when building longer discovery feeds 2. **Image Optimization**: Preload community avatars for smooth scrolling 3. **Background Updates**: Refresh discovery data in the background ## Related Topics Advanced community search and filtering capabilities Implement community membership actions from discovery Learn about community organization for better recommendations Display detailed community information before joining --- ### [Community Categories](https://learn.social.plus/social-plus-sdk/social/communities-spaces/organization/community-categories) > Query community categories and use them to filter community discovery Community categories are read through the SDK and managed in the social.plus Console. Client apps can query category lists, include or exclude deleted categories, and use category IDs when querying communities. Categories can only be created and updated from the social.plus Console. SDK access is limited to reading existing categories. ## Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `sortBy` | No | Sort order for categories. | | `includeDeleted` | No | Include deleted categories in the result. | | `limit` / pagination token | No | Page-size and pagination controls where supported by the platform. | ## Sort Options | Platform | Sort values | |----------|-------------| | TypeScript | `"name"`, `"firstCreated"`, `"lastCreated"` | | iOS | `.displayName`, `.firstCreated`, `.lastCreated` | | Android | `AmityCommunityCategorySortOption.NAME`, `FIRST_CREATED`, `LAST_CREATED` | | Flutter | `AmityCommunityCategorySortOption.NAME`, `FIRST_CREATED`, `LAST_CREATED` | ## Query Categories Use `getCategories()` to retrieve community categories. The SDK returns paginated/live collection results depending on the platform. ```swift iOS let liveCollection = communityRepository.getCategories( sortBy: .displayName, includeDeleted: false ) token = liveCollection.observe { collection, error in if let error = error { handleError(error) return } let categories = collection.snapshots // Render categories } ``` ```kotlin Android fun queryCategories(communityRepository: AmityCommunityRepository) { communityRepository .getCategories() .sortBy(sortOption = AmityCommunityCategorySortOption.NAME) .includeDeleted(includeDeleted = false) .build() .query() .doOnNext { categories: PagingData -> // Render categories } .doOnError { throwable -> // Handle error } .subscribe() } ``` ```typescript TypeScript import { CategoryRepository } from '@amityco/ts-sdk'; let nextPageFn: (() => void) | undefined; let hasMore = false; const unsubscriber = CategoryRepository.getCategories( { sortBy: 'name', includeDeleted: false, limit: 20, }, ({ data: categories, onNextPage, hasNextPage, loading, error }) => { if (error) { // Handle error return; } if (!loading && categories) { // Render categories } hasMore = hasNextPage; nextPageFn = onNextPage; }, ); function loadMoreCategories() { if (hasMore) nextPageFn?.(); } ``` ```dart Flutter final categories = []; late PagingController categoryController; void queryCommunityCategories() { categoryController = PagingController( pageFuture: (token) => AmitySocialClient.newCommunityRepository() .getCategories() .sortBy(AmityCommunityCategorySortOption.NAME) .includeDeleted(false) .getPagingData(token: token, limit: 20), pageSize: 20, )..addListener(() { if (categoryController.error == null) { categories ..clear() ..addAll(categoryController.loadedItems); } else { // Handle pagination error } }); categoryController.fetchNextPage(); } ``` ## Using Categories with Community Queries After retrieving categories, pass a category ID into community query APIs to filter community discovery. ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; const unsubscriber = CommunityRepository.getCommunities( { categoryId: 'category-id', sortBy: 'lastCreated', }, ({ data: communities }) => { // Render communities in this category }, ); ``` ```kotlin Android fun queryCommunitiesByCategory(categoryId: String) { AmitySocialClient.newCommunityRepository() .getCommunities() .categoryId(categoryId = categoryId) .build() .query() .subscribe { communities: PagingData -> // Render communities in this category } } ``` ```dart Flutter void queryCommunitiesByCategory(String categoryId) { AmitySocialClient.newCommunityRepository() .getCommunities() .categoryId(categoryId) .getLiveCollection(pageSize: 20) .getStreamController() .stream .listen((communities) { // Render communities in this category }); } ``` ## Related Topics Filter communities by category, membership, keyword, and sort order Assign categories when creating a community --- ### [Community Invitation Management](https://learn.social.plus/social-plus-sdk/social/communities-spaces/organization/community-invitation) > Read, send, accept, and reject community invitations Community invitations let moderators invite users to join a community. Depending on network settings, invited users may become members automatically or may need to accept the invitation. ## Parameters | Operation | Platforms | Parameter | Required | Description | | --- | --- | --- | --- | --- | | Get membership acceptance setting | iOS, Android, TypeScript | None | No | Reads social settings to determine whether invited users join automatically or must accept. | | Get my community invitations | iOS, Android, TypeScript | `limit` / page size | No | Page size for the signed-in user's invitation list where the platform exposes pagination. | | Check a community invitation | iOS, Android, TypeScript | Community object | Yes | Fetched community object used to read the current user's invitation state. | | Accept or reject an invitation | iOS, Android, TypeScript | Invitation object | Yes | Invitation object returned from a community or invitation query. | | Create invitations | iOS, Android, TypeScript | Community object | Yes | Fetched community object used to create invitations. | | Create invitations | iOS, Android, TypeScript | `userIds` | Yes | User IDs to invite to the community. | | Get member invitations | iOS, Android, TypeScript | Community object | Yes | Fetched community object used to query invitations sent for that community. | | Get member invitations | iOS, Android, TypeScript | `statuses` / `limit` | No | Optional invitation status filters and page size where exposed. | ## Membership Acceptance Setting Use social settings to check whether the network uses automatic membership or invitation acceptance. ```swift iOS let socialSettings = client.getSocialSettings() switch socialSettings?.membershipAcceptance { case .automatic: // Invitations add users automatically break case .invitation: // Invited users must accept break case .none: // Settings are not available locally yet break @unknown default: break } ``` ```kotlin Android fun observeMembershipAcceptance() { AmitySocialClient.getSettings() .doOnNext { socialSettings -> when (socialSettings.getMembershipAcceptanceType()) { AmityMembershipAcceptanceType.AUTOMATIC -> { // Invitations add users automatically } AmityMembershipAcceptanceType.INVITATION -> { // Invited users must accept } AmityMembershipAcceptanceType.UNKNOWN -> { // Setting is unknown } } } .subscribe() } ``` ```typescript TypeScript import { MembershipAcceptanceTypeEnum } from '@amityco/ts-sdk'; async function getMembershipAcceptance() { const settings = await client.getSocialSettings(); if (settings.membershipAcceptance === MembershipAcceptanceTypeEnum.Invitation) { // Invited users must accept } return settings.membershipAcceptance; } ``` ## Get My Community Invitations Use this flow for the current user to list pending community invitations. ```swift iOS let invitationRepository = AmityInvitationRepository() let liveCollection = invitationRepository.getMyCommunityInvitations() token = liveCollection.observe { collection, error in if let error = error { handleError(error) return } let invitations = collection.snapshots // Render pending invitations } ``` ```kotlin Android fun getMyCommunityInvitations() { AmityCoreClient.newInvitationRepository() .getMyCommunityInvitations() .doOnNext { invitations: PagingData -> // Render pending invitations } .doOnError { throwable -> // Handle error } .subscribe() } ``` ```typescript TypeScript import { InvitationRepository } from '@amityco/ts-sdk'; function getMyCommunityInvitations() { return InvitationRepository.getMyCommunityInvitations( { limit: 20 }, ({ data: invitations, loading, error }) => { if (error) { // Handle error return; } if (!loading && invitations) { // Render pending invitations } }, ); } ``` ## Check a Community Invitation Use `community.getInvitation()` when the user needs to know whether they have a pending invitation for a specific community. ```swift iOS var community: AmityCommunity! // Fetched community if let invitation = await community.getInvitation() { switch invitation.status { case .pending: // Can accept or reject break case .approved, .rejected, .canceled: // Invitation already resolved break @unknown default: break } } ``` ```kotlin Android fun getInvitation(community: AmityCommunity) { community.getInvitation() .doOnSuccess { invitations: List -> invitations.forEach { invitation -> val status = invitation.getStatus() // Render status } } .subscribe() } ``` ```typescript TypeScript import { InvitationStatusEnum } from '@amityco/ts-sdk'; async function getInvitation(community: Amity.Community) { const invitation = await community.getInvitation(); if (invitation?.status === InvitationStatusEnum.Pending) { // Can accept or reject } return invitation; } ``` ## Accept or Reject an Invitation Invitation linked objects expose `accept()` and `reject()` across TypeScript, iOS, and Android. ```swift iOS var community: AmityCommunity! // Fetched community if let invitation = await community.getInvitation() { do { try await invitation.accept() } catch let error { handleError(error) } } ``` ```kotlin Android fun acceptInvitation(invitation: AmityInvitation) { invitation.accept() .doOnComplete { // Invitation accepted } .subscribe() } ``` ```typescript TypeScript async function acceptInvitation(invitation: Amity.Invitation) { await invitation.accept(); } ``` ```swift iOS var community: AmityCommunity! // Fetched community if let invitation = await community.getInvitation() { do { try await invitation.reject() } catch let error { handleError(error) } } ``` ```kotlin Android fun rejectInvitation(invitation: AmityInvitation) { invitation.reject() .doOnComplete { // Invitation rejected } .subscribe() } ``` ```typescript TypeScript async function rejectInvitation(invitation: Amity.Invitation) { await invitation.reject(); } ``` ## Create Invitations Use `createInvitations()` on a fetched community object to invite users. ```swift iOS var community: AmityCommunity! // Fetched community do { try await community.createInvitations(["user1", "user2"]) } catch let error { handleError(error) } ``` ```kotlin Android fun createInvitations(community: AmityCommunity) { community.createInvitations(userIds = listOf("user1", "user2")) .doOnComplete { // Invitations created } .subscribe() } ``` ```typescript TypeScript async function createInvitations( community: Amity.Community, userIds: Amity.User['userId'][], ) { await community.createInvitations(userIds); } ``` ## Get Member Invitations Moderators can observe invitations sent for a community. ```swift iOS var community: AmityCommunity! // Fetched community let liveCollection = community.getMemberInvitations() token = liveCollection.observe { collection, error in if let error = error { handleError(error) return } let invitations = collection.snapshots // Render community invitations } ``` ```kotlin Android fun getMemberInvitations(community: AmityCommunity) { community.getMemberInvitations() .doOnNext { invitations: PagingData -> // Render community invitations } .subscribe() } ``` ```typescript TypeScript import { InvitationStatusEnum } from '@amityco/ts-sdk'; function getMemberInvitations(community: Amity.Community) { return community.getMemberInvitations( { statuses: [InvitationStatusEnum.Pending], limit: 20, }, ({ data: invitations, loading, error }) => { if (error) { // Handle error return; } if (!loading && invitations) { // Render community invitations } }, ); } ``` ## Related Topics Manage user-initiated joining and leaving Query and search community members Add and remove community members directly Discover and filter communities --- ### [Community Moderation](https://learn.social.plus/social-plus-sdk/social/communities-spaces/organization/community-moderation) > Assign roles, ban or unban members, and check community permissions Use community moderation APIs for role assignment, ban management, and permission checks. Role IDs must already exist in your network; these APIs assign or remove roles from users, but they do not create roles. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Add roles | `communityId` | Yes | Community ID where roles should be assigned. | | Add roles | `roleIds` / `roles` | Yes | Existing role IDs to assign. Flutter accepts one role ID per call. | | Add roles | `userIds` | Yes | User IDs that should receive the roles. | | Remove roles | `communityId` | Yes | Community ID where roles should be removed. | | Remove roles | `roleIds` / `roles` | Yes | Existing role IDs to remove. Flutter accepts one role ID per call. | | Remove roles | `userIds` | Yes | User IDs whose roles should be updated. | | Ban or unban members | `communityId` | Yes | Community ID whose ban list should change. | | Ban or unban members | `userIds` | Yes | User IDs to ban or unban. | | Check permissions | `permission` | Yes | Permission value to check, such as ban-community-user. | | Check permissions | `communityId` | Yes | Community where the permission should be evaluated. | ## Role Management TypeScript, iOS, and Android accept a list of role IDs. Flutter exposes `addRole()` and `removeRole()` for one role ID per call. ### Add Roles Add role IDs to one or more users after the roles already exist in your network. ```swift iOS let communityModeration = AmityCommunityModeration(communityId: communityId) do { try await communityModeration.addRoles( ["community-moderator"], userIds: ["user1", "user2"] ) } catch let error { handleError(error) } ``` ```kotlin Android fun addRoles(communityRepository: AmityCommunityRepository) { communityRepository .moderation(communityId = "communityId") .addRoles( roles = listOf("community-moderator"), userIds = listOf("user1", "user2") ) .doOnComplete { // Roles added } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; async function addRoles( communityId: Amity.Community['communityId'], roleIds: Amity.Role['roleId'][], userIds: Amity.User['userId'][], ): Promise { return CommunityRepository.Moderation.addRoles(communityId, roleIds, userIds); } ``` ```dart Flutter Future addRole(String communityId, List userIds) async { await AmitySocialClient.newCommunityRepository() .moderation(communityId) .addRole('community-moderator', userIds); } ``` ### Remove Roles Remove role IDs from one or more users when they should no longer have that community role. ```swift iOS let communityModeration = AmityCommunityModeration(communityId: communityId) do { try await communityModeration.removeRoles( ["community-moderator"], userIds: ["user1", "user2"] ) } catch let error { handleError(error) } ``` ```kotlin Android fun removeRoles(communityRepository: AmityCommunityRepository) { communityRepository .moderation(communityId = "communityId") .removeRoles( roles = listOf("community-moderator"), userIds = listOf("user1", "user2") ) .doOnComplete { // Roles removed } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; async function removeRoles( communityId: Amity.Community['communityId'], roleIds: Amity.Role['roleId'][], userIds: Amity.User['userId'][], ): Promise { return CommunityRepository.Moderation.removeRoles(communityId, roleIds, userIds); } ``` ```dart Flutter Future removeRole(String communityId, List userIds) async { await AmitySocialClient.newCommunityRepository() .moderation(communityId) .removeRole('community-moderator', userIds); } ``` ## Ban Members Use `banMembers()` / `banMember()` to ban users from a community. ```swift iOS let communityModeration = AmityCommunityModeration(communityId: communityId) do { try await communityModeration.banMembers(["user1", "user2"]) } catch let error { handleError(error) } ``` ```kotlin Android fun banMembers(communityRepository: AmityCommunityRepository) { communityRepository .moderation(communityId = "communityId") .banMembers(userIds = listOf("user1", "user2")) .doOnComplete { // Members banned } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; async function banMembers( communityId: Amity.Community['communityId'], userIds: Amity.User['userId'][], ) { const { data: bannedMembers } = await CommunityRepository.Moderation.banMembers(communityId, userIds); return bannedMembers; } ``` ```dart Flutter Future banMembers(String communityId, List userIds) async { await AmitySocialClient.newCommunityRepository() .moderation(communityId) .banMember(userIds); } ``` ## Unban Members Use `unbanMembers()` / `unbanMember()` to remove community bans. ```swift iOS let communityModeration = AmityCommunityModeration(communityId: communityId) do { try await communityModeration.unbanMembers(["user1", "user2"]) } catch let error { handleError(error) } ``` ```kotlin Android fun unbanMembers(communityRepository: AmityCommunityRepository) { communityRepository .moderation(communityId = "communityId") .unbanMembers(userIds = listOf("user1", "user2")) .doOnComplete { // Members unbanned } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; async function unbanMembers( communityId: Amity.Community['communityId'], userIds: Amity.User['userId'][], ) { const { data: unbannedMembers } = await CommunityRepository.Moderation.unbanMembers(communityId, userIds); return unbannedMembers; } ``` ```dart Flutter Future unbanMembers(String communityId, List userIds) async { await AmitySocialClient.newCommunityRepository() .moderation(communityId) .unbanMember(userIds); } ``` ## Check Permissions Permission checks use the current user's cached permission state. Query the relevant community/member state before relying on the result for UI decisions. ```swift iOS let canBan = await client.hasPermission( .banCommunityUser, forCommunity: communityId ) ``` ```kotlin Android fun checkCommunityPermission() { AmityCoreClient .hasPermission(permission = AmityPermission.BAN_COMMUNITY_USER) .atCommunity(communityId = "communityId") .check() .doOnNext { hasPermission: Boolean -> // Render permission-aware UI } .subscribe() } ``` ```typescript TypeScript function checkCommunityPermission(communityId: Amity.Community['communityId']) { return client .hasPermission(Amity.Permission.BanChannelCommunityPermission) .community(communityId); } ``` ```dart Flutter void checkCommunityPermission(String communityId) { final hasPermission = AmityCoreClient .hasPermission(AmityPermission.BAN_COMMUNITY_USER) .atCommunity(communityId) .check(); } ``` ## Related Topics Add and remove community members User-initiated joining, leaving, and approval workflows Search and filter community member lists Invitation-based member onboarding workflows --- ### [Join/Leave Community](https://learn.social.plus/social-plus-sdk/social/communities-spaces/organization/join-leave-community) > Join or leave communities, handle join requests, and manage approval flows Use community membership APIs to let the active user join or leave a community. Communities that require approval return a pending join request instead of immediate membership. iOS, Android, and TypeScript support object-based `community.join()` flows that return a success or pending result. Flutter currently exposes repository-level `joinCommunity()` and `leaveCommunity()` methods. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Join community | Community object | Platform-dependent | iOS, Android, and TypeScript call `join()` on a fetched community object. | | Join community | `communityId` | Platform-dependent | Flutter joins by community ID. | | Check my join request | Community object | Yes | Fetched community object used to read the active user's pending request. | | Observe pending join requests | Community object | Yes | Fetched community object used by moderators to list join requests. | | Observe pending join requests | `status` / `limit` | No | Optional request status and page-size controls where exposed. | | Cancel, approve, or reject | Join request object | Yes | Join request object returned from a join-request query. | | Leave community | `communityId` | Yes | Community ID the active user should leave. | ## Join Community For approval-aware flows, fetch a community object first and call `join()` where the platform supports it. The result tells you whether membership was granted immediately or a join request is pending. ```swift iOS var community: AmityCommunity! // Fetched community Task { @MainActor in do { let result = try await community.join() switch result { case .success: // Joined immediately break case .pending(let joinRequest): let status = joinRequest.status // Show pending state } } catch let error { handleError(error) } } ``` ```kotlin Android fun joinCommunity(community: AmityCommunity) { community.join() .doOnSuccess { result -> when (result) { is AmityJoinResult.Success -> { // Joined immediately } is AmityJoinResult.Pending -> { val joinRequest = result.request // Show pending state } } } .doOnError { throwable -> when (AmityError.from(throwable)) { AmityError.ITEM_NOT_FOUND -> { // Community does not exist } AmityError.USER_IS_BANNED -> { // Current user is banned } else -> { // Handle other errors } } } .subscribe() } ``` ```typescript TypeScript async function joinCommunity(community: Amity.Community): Promise { const result = await community.join(); if (result.status === 'pending') { const joinRequest = result.request; // Show pending state } return result; } ``` ```dart Flutter Future joinCommunity(String communityId) async { await AmitySocialClient.newCommunityRepository() .joinCommunity(communityId); } ``` ## Join Requests When a community requires approval, the SDK exposes the active user's request and moderator review flows. ### Check My Join Request Fetch the active user's join request when an approval-required community returns a pending state. ```swift iOS var community: AmityCommunity! // Fetched community Task { @MainActor in do { let joinRequest = try await community.getMyJoinRequest() let status = joinRequest.status // Render status } catch let error { handleError(error) } } ``` ```kotlin Android fun getMyJoinRequest(community: AmityCommunity) { community.getMyJoinRequest() .doOnSuccess { joinRequest -> val status = joinRequest.getStatus() // Render status } .doOnError { throwable -> // Handle error } .subscribe() } ``` ```typescript TypeScript async function getMyJoinRequest(community: Amity.Community) { const joinRequest = await community.getMyJoinRequest(); if (joinRequest?.status === 'pending') { // Render pending state } return joinRequest; } ``` ### Observe Pending Join Requests Moderators can observe join requests for a community and approve or reject individual requests. ```swift iOS var community: AmityCommunity! // Fetched community token = community.getJoinRequests(status: .pending).observe { collection, error in if let error = error { handleError(error) return } let joinRequests = collection.snapshots // Render pending requests } ``` ```kotlin Android fun observePendingJoinRequests(community: AmityCommunity) { community.getJoinRequests(status = AmityJoinRequestStatus.PENDING) .doOnNext { joinRequests: PagingData -> // Render pending requests } .doOnError { throwable -> // Handle error } .subscribe() } ``` ```typescript TypeScript import { JoinRequestStatusEnum } from '@amityco/ts-sdk'; function observePendingJoinRequests(community: Amity.Community) { return community.getJoinRequests( { communityId: community.communityId, type: 'communityJoinRequest', targetType: 'community', status: JoinRequestStatusEnum.Pending, options: { limit: 20 }, }, ({ data: joinRequests, loading, error }) => { if (error) { // Handle error return; } if (!loading && joinRequests) { // Render pending requests } }, ); } ``` ### Cancel, Approve, or Reject Cancel your own request or approve and reject pending requests from a moderator flow. ```swift iOS func cancelJoinRequest(_ joinRequest: AmityJoinRequest) { Task { @MainActor in do { try await joinRequest.cancel() } catch let error { handleError(error) } } } func approveJoinRequest(_ joinRequest: AmityJoinRequest) { Task { @MainActor in do { try await joinRequest.approve() } catch let error { handleError(error) } } } func rejectJoinRequest(_ joinRequest: AmityJoinRequest) { Task { @MainActor in do { try await joinRequest.reject() } catch let error { handleError(error) } } } ``` ```kotlin Android fun cancelJoinRequest(joinRequest: AmityJoinRequest) { joinRequest.cancel() .doOnComplete { // Cancelled } .subscribe() } fun approveJoinRequest(joinRequest: AmityJoinRequest) { joinRequest.approve() .doOnComplete { // Approved } .subscribe() } fun rejectJoinRequest(joinRequest: AmityJoinRequest) { joinRequest.reject() .doOnComplete { // Rejected } .subscribe() } ``` ```typescript TypeScript async function cancelJoinRequest(joinRequest: Amity.JoinRequest) { await joinRequest.cancel(); } async function approveJoinRequest(joinRequest: Amity.JoinRequest) { await joinRequest.approve(); } async function rejectJoinRequest(joinRequest: Amity.JoinRequest) { await joinRequest.reject(); } ``` ## Leave Community Use `leaveCommunity()` to remove the active user from a community. Leaving removes the current user's community membership. If the community requires approval, the user may need to request access again before rejoining. ```swift iOS do { try await communityRepository.leaveCommunity(withId: communityId) } catch let error { handleError(error) } ``` ```kotlin Android fun leaveCommunity(communityRepository: AmityCommunityRepository, communityId: String) { communityRepository .leaveCommunity(communityId = communityId) .doOnComplete { // Left community } .doOnError { throwable -> when (AmityError.from(throwable)) { AmityError.ITEM_NOT_FOUND -> { // Community does not exist } AmityError.PERMISSION_DENIED -> { // Permission denied } else -> { // Handle other errors } } } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; async function leaveCommunity(communityId: Amity.Community['communityId']) { const hasLeft = await CommunityRepository.leaveCommunity(communityId); return hasLeft; } ``` ```dart Flutter Future leaveCommunity(String communityId) async { await AmitySocialClient.newCommunityRepository() .leaveCommunity(communityId); } ``` ## Related Topics View and search community member lists. Add and remove community members. Manage roles, bans, and permissions. Invitation-based member onboarding workflows. --- ### [Member Management](https://learn.social.plus/social-plus-sdk/social/communities-spaces/organization/member-management) > Add and remove community members with the community membership APIs Use member management APIs when a moderator or admin needs to add users to a community or remove existing members. These APIs are separate from user-initiated join and leave flows. Member management APIs require the current user to have the relevant community permissions. The SDK methods perform the operation; your app should query member lists afterward if the UI needs an updated collection. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Add members | `communityId` | Yes | Community ID whose membership should change. | | Add members | `userIds` | Yes | User IDs to add to the community. | | Remove members | `communityId` | Yes | Community ID whose membership should change. | | Remove members | `userIds` | Yes | User IDs to remove from the community. | ## Add Members Use `addMembers()` to add one or more users to a community. ```swift iOS let communityMembership = AmityCommunityMembership(communityId: communityId) do { try await communityMembership.addMembers(["user1", "user2"]) } catch let error { handleError(error) } ``` ```kotlin Android fun addMembers(communityRepository: AmityCommunityRepository) { communityRepository .membership(communityId = "communityId") .addMembers(userIds = listOf("user1", "user2")) .doOnComplete { // Members added } .doOnError { throwable -> // Handle error } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; async function addMembers( communityId: Amity.Community['communityId'], userIds: Amity.User['userId'][], ): Promise { return CommunityRepository.Membership.addMembers(communityId, userIds); } ``` ```dart Flutter Future addMembers(String communityId, List userIds) async { await AmitySocialClient.newCommunityRepository() .membership(communityId) .addMembers(userIds); } ``` ## Remove Members Use `removeMembers()` to remove one or more users from a community. Removing members changes their community membership immediately. If the community requires approval, removed users may need to request access again before rejoining. ```swift iOS let communityMembership = AmityCommunityMembership(communityId: communityId) do { try await communityMembership.removeMembers(["user1", "user2"]) } catch let error { handleError(error) } ``` ```kotlin Android fun removeMembers(communityRepository: AmityCommunityRepository) { communityRepository .membership(communityId = "communityId") .removeMembers(userIds = listOf("user1", "user2")) .doOnComplete { // Members removed } .doOnError { throwable -> // Handle error } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; async function removeMembers( communityId: Amity.Community['communityId'], userIds: Amity.User['userId'][], ): Promise { return CommunityRepository.Membership.removeMembers(communityId, userIds); } ``` ```dart Flutter Future removeMembers(String communityId, List userIds) async { await AmitySocialClient.newCommunityRepository() .membership(communityId) .removeMembers(userIds); } ``` ## Related Topics Assign roles and ban or unban community members User-initiated membership workflows and approval requests Search and filter community member lists Invitation-based member onboarding workflows --- ### [Query Community Members](https://learn.social.plus/social-plus-sdk/social/communities-spaces/organization/query-community-members) > Query and search community members by membership status, roles, keyword, and sort order Use member query APIs to list community members and member search APIs to find members by keyword. The available filters and sort values differ by SDK, so use the platform-specific values below. Member query APIs return paginated or live collection results depending on the platform. Use pagination controls for large communities. ## Parameters ### Common Parameters | Parameter | Description | |-----------|-------------| | `communityId` | Community to query. | | `roles` | Optional role IDs to include, such as `community-moderator`. | | `includeDeleted` | Include deleted users when supported. | | `excludingRoles` | Exclude members with any role in the list. Available in TypeScript, iOS, and Android. | | `limit` / pagination token | Page-size and pagination controls where supported. | ### Query Filters | Platform | Values | |----------|--------| | TypeScript | `memberships: ["member"]`, `["banned"]`, or omit `memberships` for all results | | iOS | `AmityCommunityMembership.QueryFilter.member`, `.banned`, `.all` | | Android | `AmityCommunityMembershipFilter.MEMBER`, `BANNED`, `ALL` | | Flutter | `AmityCommunityMembershipFilter.MEMBER`, `BANNED`, `ALL` | ### Query Sort Options | Platform | Values | |----------|--------| | TypeScript | `"firstCreated"`, `"lastCreated"` | | iOS | `.displayName`, `.firstCreated`, `.lastCreated`, `.lastJoin` | | Android | `DISPLAY_NAME`, `FIRST_CREATED`, `LAST_CREATED`, `LAST_JOIN` | | Flutter | `FIRST_CREATED`, `LAST_CREATED` | Query members when you need a paginated member list with membership, role, deletion, and sort filters. ```swift iOS let communityMembership = AmityCommunityMembership(communityId: communityId) let liveCollection = communityMembership.getMembers( filter: .member, roles: ["community-moderator"], sortBy: .firstCreated, includeDeleted: false, excludingRoles: ["channel-moderator"] ) token = liveCollection.observe { collection, error in if let error = error { handleError(error) return } let members = collection.snapshots // Render members } ``` ```kotlin Android fun queryCommunityMembers(communityRepository: AmityCommunityRepository) { communityRepository .membership(communityId = "communityId") .getMembers() .filter(filter = AmityCommunityMembershipFilter.MEMBER) .roles(roles = listOf("community-moderator")) .excludingRoles(excludingRoles = listOf("channel-moderator")) .includeDeleted(includeDeleted = false) .sortBy(sortBy = AmityCommunityMembershipSortOption.FIRST_CREATED) .build() .query() .doOnNext { members: PagingData -> // Render members } .doOnError { throwable -> // Handle error } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; let nextPageFn: (() => void) | undefined; let hasMore = false; const unsubscriber = CommunityRepository.Membership.getMembers( { communityId, memberships: ['member'], roles: ['community-moderator'], excludingRoles: ['channel-moderator'], sortBy: 'firstCreated', includeDeleted: false, limit: 20, }, ({ data: members, onNextPage, hasNextPage, loading, error }) => { if (error) { // Handle error return; } if (!loading && members) { // Render members } hasMore = hasNextPage; nextPageFn = onNextPage; }, ); function loadMoreMembers() { if (hasMore) nextPageFn?.(); } ``` ```dart Flutter final members = []; late PagingController membersController; void queryCommunityMembers(String communityId) { membersController = PagingController( pageFuture: (token) => AmitySocialClient.newCommunityRepository() .membership(communityId) .getMembers() .filter(AmityCommunityMembershipFilter.MEMBER) .roles(['community-moderator']) .includeDeleted(false) .sortBy(AmityCommunityMembershipSortOption.FIRST_CREATED) .getPagingData(token: token, limit: 20), pageSize: 20, )..addListener(() { if (membersController.error == null) { members ..clear() ..addAll(membersController.loadedItems); } else { // Handle pagination error } }); } ``` ## Search Community Members Search APIs use the same community and role controls, plus a keyword/display-name search input. ### Search Filters | Platform | Values | |----------|--------| | TypeScript | `memberships: ["member"]`, `["banned"]`, or omit `memberships` | | iOS | `AmityCommunityMembership.SearchFilter.member`, `.banned` | | Android | `AmityCommunityMembership.MEMBER`, `BANNED` | | Flutter | `AmityCommunityMembershipFilter.MEMBER`, `BANNED`, `ALL` | ### Search Sort Options | Platform | Values | |----------|--------| | TypeScript | `"displayName"`, `"firstCreated"`, `"lastCreated"` | | iOS | `.displayName`, `.firstCreated`, `.lastCreated`, `.lastJoin` | | Android | `DISPLAY_NAME`, `FIRST_CREATED`, `LAST_CREATED`, `LAST_JOIN` | | Flutter | `FIRST_CREATED`, `LAST_CREATED` | ```swift iOS let communityMembership = AmityCommunityMembership(communityId: communityId) let liveCollection = communityMembership.searchMembers( keyword: "alex", filter: [.member], roles: ["community-moderator"], sortBy: .displayName, includeDeleted: false, excludingRoles: ["channel-moderator"] ) token = liveCollection.observe { collection, error in if let error = error { handleError(error) return } let members = collection.snapshots // Render matching members } ``` ```kotlin Android fun searchCommunityMembers( communityRepository: AmityCommunityRepository, keyword: String ) { communityRepository .membership(communityId = "communityId") .searchMembers(keyword = keyword) .roles(roles = listOf("community-moderator")) .membershipFilter( communityMembership = listOf(AmityCommunityMembership.MEMBER) ) .excludingRoles(excludingRoles = listOf("channel-moderator")) .includeDeleted(includeDeleted = false) .sortBy(AmityCommunityMembershipSortOption.DISPLAY_NAME) .build() .query() .doOnNext { members: PagingData -> // Render matching members } .subscribe() } ``` ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; function searchCommunityMembers(keyword: string) { return CommunityRepository.Membership.searchMembers( { communityId, search: keyword, memberships: ['member'], roles: ['community-moderator'], excludingRoles: ['channel-moderator'], sortBy: 'displayName', includeDeleted: false, limit: 20, }, ({ data: members, loading, error }) => { if (error) { // Handle error return; } if (!loading && members) { // Render matching members } }, ); } ``` ```dart Flutter final searchResults = []; late PagingController searchController; void searchCommunityMembers(String communityId, String keyword) { searchController = PagingController( pageFuture: (token) => AmitySocialClient.newCommunityRepository() .membership(communityId) .searchMembers(keyword) .filter(AmityCommunityMembershipFilter.MEMBER) .roles(['community-moderator']) .includeDeleted(false) .sortBy(AmityCommunityMembershipSortOption.FIRST_CREATED) .getPagingData(token: token, limit: 20), pageSize: 20, )..addListener(() { if (searchController.error == null) { searchResults ..clear() ..addAll(searchController.loadedItems); } else { // Handle pagination error } }); } ``` ## Excluding Roles Use `excludingRoles` when you need a member list that removes users with any role in the exclusion list. This filter is available in TypeScript, iOS, and Android. The current Flutter member query builder does not expose `excludingRoles`. ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; const unsubscriber = CommunityRepository.Membership.getMembers( { communityId, excludingRoles: ['community-moderator', 'channel-moderator'], limit: 20, }, ({ data: members }) => { // Members do not include users with excluded roles }, ); ``` ```swift iOS let communityMembership = AmityCommunityMembership(communityId: communityId) let liveCollection = communityMembership.getMembers( filter: .member, roles: [], sortBy: .firstCreated, includeDeleted: false, excludingRoles: ["community-moderator", "channel-moderator"] ) token = liveCollection.observe { collection, error in let members = collection.snapshots // Members do not include users with excluded roles } ``` ```kotlin Android fun queryMembersExcludingRoles(communityRepository: AmityCommunityRepository) { communityRepository .membership(communityId = "communityId") .getMembers() .filter(filter = AmityCommunityMembershipFilter.MEMBER) .excludingRoles( excludingRoles = listOf("community-moderator", "channel-moderator") ) .build() .query() .doOnNext { members: PagingData -> // Members do not include users with excluded roles } .subscribe() } ``` ## Related Topics Manage joining and leaving communities. Add and remove community members. Manage roles, bans, and permissions. Understand role-based access control. --- ### [User Relationship](https://learn.social.plus/social-plus-sdk/social/user-relationship/overview) > SDK overview for follow, unfollow, follow requests, follower lists, and blocking. User relationship APIs let a signed-in user build and manage their social graph. Use them for follow/unfollow actions, incoming follow requests, follower and following lists, follow counts, and blocked-user management. For an end-to-end product walkthrough, see [User Profiles & Social Graph](/use-cases/social/user-profiles-and-social-graph). This section focuses on SDK calls and return shapes. ## Relationship Systems | System | What it manages | Main SDK surface | | --- | --- | --- | | Following | Follow requests, accepted follows, pending requests, follower/following lists, and follow counts | `UserRepository.Relationship` on TypeScript, `relationship()` on Android and Flutter, `AmityUserRelationship` on iOS | | Blocking | Block/unblock actions, the current user's blocked-user list, and — in the reverse direction — the users who have blocked the current user | Relationship block APIs plus `getBlockedUsers()` / `getBlockingUsers()` on the user repository | Follow status values are platform-specific enum or string values, but the shared meanings are: | Status | Meaning | | --- | --- | | `none` | No active follow relationship | | `pending` | A follow request exists and is waiting for action | | `accepted` | The follow relationship is active | | `blocked` | The relationship is blocked | Whether a follow becomes `accepted` immediately or starts as `pending` is determined by your network's follow configuration. The same SDK follow call is used in both cases. ## Platform Coverage | Capability | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Follow and unfollow | Yes | Yes | Yes | Yes | | Accept and decline incoming follow requests | Yes | Yes | Yes | Yes | | Get current user's follow counts | Yes | Yes | Yes | Yes | | Get another user's follow counts and status | Yes | Yes | Yes | Yes | | Query followers and following | Yes | Yes | Yes | Yes | | Block and unblock users | Yes | Yes | Yes | Yes | | Query paginated blocked users | Yes | Yes | Yes | Yes | | Get all blocked users as a one-shot list | Yes | Yes | Yes | Not available in the Flutter public repository | | Query users who blocked the current user (reverse direction) | Yes | Yes | Yes | Not available in the current Flutter SDK | ## Use The Right Read API Use the live or paginated APIs for user-facing screens: - Follow info: profile headers, follow buttons, counters, and relationship badges. - Follower/following lists: people lists with pagination and refresh behavior. - Blocked users: a settings screen where users can review and unblock accounts. Use `getAllBlockedUsers()` only when you need a one-shot block list for local decisions. It returns up to 100 blocked users on TypeScript, iOS, and Android, and the SDK caches the result briefly. Do not rely on this overview for feed visibility, search visibility, comment permissions, or other product policy details. Those behaviors can depend on backend configuration and feature area. Handle SDK errors from the affected feature API and keep the UI state in sync with relationship status. ## Related Guides Create or remove a follow relationship. Approve or reject incoming follow requests. Read follow status and follow counts. Query paginated relationship lists. Block or unblock another user. Query the current user's blocked-user list. Query the reverse direction — users who blocked you. ## Social — Content Management Overview ### [Overview](https://learn.social.plus/social-plus-sdk/social/content-management/overview) > Create, retrieve, moderate, and measure posts, comments, stories, and share links with the Social+ SDKs Content Management is the SDK surface for user-generated social content. Use it to build feeds, post creation flows, comment threads, story experiences, content reporting, review queues, and shareable links. The exact method names and supported content types vary by platform, so the pages in this section show platform-specific snippets instead of treating every SDK as identical. Create text, media, poll, live, room, mixed, and custom posts where supported by each SDK. Add, query, edit, and delete comments and replies on posts or other supported targets. Publish and retrieve short-lived story content for users and communities. Fetch network-level shareable-link configuration for supported content types. Let users flag content and let moderators review or manage posts where the SDK exposes review APIs. Mark posts and stories as viewed and read engagement counters exposed by the SDK. ## Platform Coverage | Area | Current SDK coverage | | --- | --- | | Posts | TypeScript, iOS, Android, and Flutter. Supported post types differ by SDK; see [Posts Overview](./posts/overview). | | Comments | TypeScript, iOS, Android, and Flutter. | | Stories | TypeScript, iOS, Android, and Flutter. | | Shareable links | TypeScript, iOS, and Android expose shareable-link configuration. A Flutter public API was not found in the current Flutter SDK source. | | Post `structureType` | TypeScript, iOS, and Android expose this field. Flutter's current public post model exposes `type` but not `structureType`. | | Post `localCommentCount` | TypeScript, iOS, and Android expose a locally computed live comment count. Flutter's current public post model does not expose this field. | This section documents SDK behavior only. Console configuration, API-only workflows, and UIKit behavior can differ and are covered elsewhere. ## Integration Path Decide whether the content belongs to a user feed, a community feed, or another target supported by the page you are implementing. Image, video, file, audio, and clip posts reference uploaded files. Use the content-handling upload pages before calling the post creation APIs. Use the post, comment, or story repository for the content type. Prefer the platform-specific page for exact method names and return types. Use live objects or live collections for feeds, detail views, and comment threads that should update while the user is looking at them. Add flagging, review, delete, pin, and impression tracking only where your product workflow and platform SDK support those actions. ## Related Topics Start with text posts, then add media or custom post types. Load feeds and filter by target, type, status, or mixed media structure. Subscribe to social topics when the UI needs remote updates. --- ### [Content Sharing](https://learn.social.plus/social-plus-sdk/social/content-management/content-sharing) > Fetch shareable-link configuration and build links for supported social content Shareable links are configured at the network level. The SDKs fetch the configured domain and URL patterns so your app can build links to Social+ content using your own routing scheme. The current SDKs expose this configuration differently: | Platform | SDK surface | | --- | --- | | TypeScript | `Client.getShareableLinkConfiguration()` returns helper methods such as `generateLink`, `getPattern`, and `isEnabled`. | | iOS | `client.getShareableLinkConfiguration()` returns `domain` and `patterns`. | | Android | `AmityCoreClient.getShareableLinkConfiguration().getShareableLink()` returns `domain` and `patterns`. | | Flutter | No public shareable-link configuration API was found in the current Flutter SDK source. | The SDK reads the configured patterns. It does not create the destination screens in your app. Your app still needs routes that can open the generated URLs. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Fetch configuration | None | No | Reads the network-level shareable-link configuration. | | Generate a post link | `postId` | Yes | Post ID used to replace `{postId}` in the configured URL pattern. | | Generate a post link | `posts` pattern | Yes | Pattern returned by the backend configuration for post links. | | Generate a post link | `domain` | Yes | Configured shareable-link domain returned by the SDK. | ## Supported Pattern Keys TypeScript maps content types to these pattern keys: | Content type | Pattern key | Placeholder | | --- | --- | --- | | Post | `posts` | `{postId}` | | Community | `communities` | `{communityId}` | | User | `users` | `{userId}` | | Livestream | `livestream` | `{livestream}` | | Event | `events` | `{eventId}` | iOS and Android return the backend `patterns` map directly. Check that the key you need exists before generating a link. ## Generate a Post Link Fetch the shareable-link configuration before generating a post link, then fall back gracefully if the needed pattern is missing. ```typescript TypeScript import { AmitySharableContentType, Client } from "@amityco/ts-sdk"; async function getPostShareLink(postId: string) { const config = await Client.getShareableLinkConfiguration(); return config.generateLink(AmitySharableContentType.POST, postId); } ``` ```swift iOS let postId = "post-id" let config = try await client.getShareableLinkConfiguration() if let pattern = config.patterns["posts"] { let link = config.domain + pattern.replacingOccurrences( of: "{postId}", with: postId ) showInfo(link) } ``` ```kotlin Android AmityCoreClient.getShareableLinkConfiguration() .getShareableLink() .subscribe({ config -> val pattern = config.getPatterns()["posts"] val link = pattern?.let { config.getDomain() + it.replace("{postId}", postId) } }, { error -> handleGeneralError(error) }) ``` ## Generate an Event Link Event links are the primary use case for shareable links: an app can copy or share a direct link to an event detail screen. Gate the share UI on `isEnabled` first, then generate the link — `generateLink` returns `null` when the event pattern is not configured. ```typescript TypeScript import { AmitySharableContentType, Client } from "@amityco/ts-sdk"; async function getEventShareLink(eventId: string) { const config = await Client.getShareableLinkConfiguration(); // Feature gate — hide the share action when event links aren't configured if (!config.isEnabled(AmitySharableContentType.EVENT)) return null; return config.generateLink(AmitySharableContentType.EVENT, eventId); // → "https://app.example.com/events/abc123" or null } ``` ```swift iOS let eventId = "event-id" let config = try await client.getShareableLinkConfiguration() // Feature gate — hide the share action when event links aren't configured guard config.isEnabled(.event) else { return } if let link = config.generateLink(.event, referenceId: eventId) { showInfo(link) } ``` ```kotlin Android AmityCoreClient.getShareableLinkConfiguration() .subscribe({ config -> // Feature gate — hide the share action when event links aren't configured if (config.isEnabled(AmitySharableContentType.EVENT)) { val link = config.generateLink(AmitySharableContentType.EVENT, eventId) // ... copy or share `link` } }, { error -> handleGeneralError(error) }) ``` Shareable links are available on TypeScript, iOS, and Android. Flutter has no public shareable-link configuration API in the current SDK source. ## Best Practices - Treat missing patterns as a disabled share target for that content type. - Generate links only after the content exists and you have the final content ID. - Keep app route handling separate from link generation so deleted or restricted content can show a useful fallback screen. - Cache the configuration during a session if your sharing UI needs it often. ## Related Topics Learn how posts are modeled and created. Share community destinations after configuring community URL patterns. Share user profile destinations after configuring user URL patterns. Share event links after configuring the event URL pattern. --- ### [Content Moderation Overview](https://learn.social.plus/social-plus-sdk/social/content-management/moderation/overview) > SDK moderation surfaces for flagging, review, deletion, community governance, and user safety. Use social.plus moderation APIs to collect user reports, review posts before publication, remove content, and enforce community rules. This overview maps the SDK surfaces; Console review workflows and policy decisions live outside the SDK. Let users flag or unflag posts and comments, then check flag status in the UI. Query reviewing posts and approve or decline them where post review is enabled. Remove posts or comments after a user action or moderation decision. Assign roles, ban members, unban members, and check community permissions. ## Choose the Right API | Goal | Start here | | --- | --- | | Users report a post or comment | [Content Flagging](./content-flagging) | | Moderators approve or decline posts in review | [Post Review](../posts/moderation/post-review) | | Moderators remove posts | [Delete Post](../posts/moderation/delete-post) | | Moderators remove comments | [Delete Comment](../comments/actions/delete-comment) | | Community owners manage member permissions | [Community Moderation](../../communities-spaces/organization/community-moderation) | | Users block abusive accounts | [Block & Unblock User](../../user-relationship/blocking/block-unblock-user) | The SDK exposes the actions and status checks listed above. It does not define your community policy, escalation rules, moderator assignment process, or user appeal flow. ## Implementation Notes - Keep moderation UI state local until the SDK call succeeds. - Refresh the affected live object or collection after approving, declining, deleting, flagging, or unflagging content. - Use permission checks before showing moderator-only actions. - Keep user-facing labels for flag reasons aligned with your community guidelines. ## Related Topics Add report and unreport actions for posts and comments. Work with review queues and post approval state. --- ### [Content Flagging](https://learn.social.plus/social-plus-sdk/social/content-management/moderation/content-flagging) > Flag, unflag, and check flag status for posts and comments. Use content flagging APIs when users need to report posts or comments for review. The SDK supports flagging, unflagging, and checking whether the current user has flagged a loaded item. TypeScript, iOS, and Android support reasoned flagging for posts and comments. Flutter supports post and comment flag/unflag actions, but the current public post/comment flag builders do not accept a reason parameter. ## SDK surfaces Flag and unflag posts through the post repository or post model extension, depending on platform. Flag and unflag comments through the comment repository or comment model extension. Use predefined reasons such as spam, harassment, violence, or a custom "Others" detail where supported. Check whether the current user has flagged a loaded post or comment before rendering an unflag action. ## Flag reasons | Reason | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Community guidelines | `ContentFlagReasonEnum.CommunityGuidelines` | `.communityGuidelines` | `AmityContentFlagReason.CommunityGuidelines` | Reason enum exists; post/comment flag builders do not accept it | | Harassment or bullying | `ContentFlagReasonEnum.HarassmentOrBullying` | `.harassmentOrBullying` | `AmityContentFlagReason.HarassmentOrBullying` | Reason enum exists; post/comment flag builders do not accept it | | Spam or scams | `ContentFlagReasonEnum.SpamOrScams` | `.spamOrScams` | `AmityContentFlagReason.SpamOrScams` | Reason enum exists; post/comment flag builders do not accept it | | Other custom detail | Any custom string | `.others("...")` | `AmityContentFlagReason.Others("...")` | Reason enum exists; post/comment flag builders do not accept it | ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Flag a post | `postId` | Yes | Post ID to report. | | Flag a post | `reason` | Depends | Required on TypeScript, iOS, and Android reasoned flagging APIs; not accepted by the current public Flutter post flag builder. | | Flag a comment | `commentId` | Yes | Comment ID to report. | | Flag a comment | `reason` | Depends | Required on TypeScript, iOS, and Android reasoned flagging APIs; not accepted by the current public Flutter comment flag builder. | | Unflag content | `postId` / `commentId` | Yes | Content ID whose current-user report should be removed. | | Check flag status | `postId` / `commentId` | Yes | Content ID or loaded model used to check whether the current user has flagged the item. | ## Flag a post Flag a post with a supported reason where the platform exposes reasoned flagging. ```typescript TypeScript import { ContentFlagReasonEnum, PostRepository } from '@amityco/ts-sdk'; const didFlag = await PostRepository.flagPost( postId, ContentFlagReasonEnum.SpamOrScams, ); if (didFlag) { showSuccessMessage('Post flagged'); } ``` ```swift iOS let repository = AmityPostRepository() try await repository.flagPost( withId: "post-id", reason: .spamOrScams ) ``` ```kotlin Android val disposable = AmitySocialClient.newPostRepository() .flagPost( postId = postId, reason = AmityContentFlagReason.SpamOrScams, ) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final didFlag = await post.report().flag(); if (didFlag) { // Show flagged state. } ``` ## Flag a comment Flag a comment with a supported reason where the platform exposes reasoned flagging. ```typescript TypeScript import { CommentRepository, ContentFlagReasonEnum } from '@amityco/ts-sdk'; const didFlag = await CommentRepository.flagComment( commentId, ContentFlagReasonEnum.HarassmentOrBullying, ); if (didFlag) { showSuccessMessage('Comment flagged'); } ``` ```swift iOS let repository = AmityCommentRepository() try await repository.flagComment( withId: "comment-id", reason: .harassmentOrBullying ) ``` ```kotlin Android val disposable = AmitySocialClient.newCommentRepository() .flagComment( commentId = commentId, reason = AmityContentFlagReason.HarassmentOrBullying, ) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final didFlag = await comment.report().flag(); if (didFlag) { // Show flagged state. } ``` ## Unflag content Unflag content when the current user removes their own report from a post or comment. ```typescript TypeScript import { CommentRepository, PostRepository } from '@amityco/ts-sdk'; const didUnflagPost = await PostRepository.unflagPost(postId); const didUnflagComment = await CommentRepository.unflagComment(commentId); ``` ```swift iOS let postRepository = AmityPostRepository() let commentRepository = AmityCommentRepository() try await postRepository.unflagPost(withId: "post-id") try await commentRepository.unflagComment(withId: "comment-id") ``` ```kotlin Android val postDisposable = AmitySocialClient.newPostRepository() .unflagPost(postId = postId) .subscribe() val commentDisposable = AmitySocialClient.newCommentRepository() .unflagComment(commentId = commentId) .subscribe() ``` ```dart Flutter final didUnflagPost = await post.report().unflag(); final didUnflagComment = await comment.report().unflag(); ``` ## Check flag status Check flag status before rendering a flag or unflag action for the loaded item. ```typescript TypeScript import { CommentRepository, PostRepository } from '@amityco/ts-sdk'; const isPostFlagged = await PostRepository.isPostFlaggedByMe(postId); const isCommentFlagged = await CommentRepository.isCommentFlaggedByMe(commentId); ``` ```swift iOS let postRepository = AmityPostRepository() let commentRepository = AmityCommentRepository() let isPostFlagged = try await postRepository.isFlaggedByMe(withId: "post-id") let isCommentFlagged = try await commentRepository.isCommentFlaggedByMe(withId: "comment-id") ``` ```kotlin Android val loadedPost = post ?: return val loadedComment = comment ?: return val isPostFlagged = loadedPost.isFlaggedByMe() val isCommentFlagged = loadedComment.isFlaggedByMe() ``` ```dart Flutter final isPostFlagged = post.isFlaggedByMe; final isCommentFlagged = comment.isFlaggedByMe; ``` Flagging creates report state; it does not delete, hide, approve, or decline content by itself. For review workflows, use the moderation tools available in Console and the SDK APIs for the content type you are moderating. ## Related topics Approve or decline posts in review workflows. Remove posts after a moderation decision. Remove comments after a moderation decision. Manage community roles, bans, and permissions. ## Social — Posts ### [Posts Overview](https://learn.social.plus/social-plus-sdk/social/content-management/posts/overview) > Understand post targets, content types, structure types, live comment counts, and the platform-specific post APIs Posts are the primary content objects in Social+. A post belongs to a target, such as a user feed or community feed, and can contain text, uploaded media, poll data, live or room references, or custom data depending on the SDK. Looking for a product walkthrough? The [Rich Content Creation](/use-cases/social/rich-content-creation) guide covers an end-to-end content flow. This page focuses on SDK surfaces and data behavior. ## Post Types | Type | Current SDK notes | | --- | --- | | Text | Supported by TypeScript, iOS, Android, and Flutter. | | Image | Supported by TypeScript, iOS, Android, and Flutter after image upload. | | Video | Supported by TypeScript, iOS, Android, and Flutter after video upload. | | File | Supported by TypeScript, iOS, Android, and Flutter after file upload. | | Poll | Supported by TypeScript, iOS, Android, and Flutter when a poll exists. | | Live stream or room | TypeScript and Android expose both live stream and room data types. iOS exposes room creation and deprecated live-stream creation. Flutter exposes live-stream post creation in the current source. | | Audio | Supported by TypeScript, iOS, and Android after audio upload. A Flutter audio post creator was not found in the current source. | | Clip | Supported by TypeScript, iOS, and Android after clip upload. A Flutter clip post creator was not found in the current source. | | Mixed media | TypeScript, iOS, and Android expose mixed attachment/media flows. A Flutter mixed-media post creator was not found in the current source. | | Custom | Supported by TypeScript, iOS, Android, and Flutter for app-specific data. | ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Create a text post | `targetType` | Yes | Feed target type, commonly `community` or `user`. | | Create a text post | `targetId` | Yes | Target feed ID, such as a community ID or user ID. | | Create a text post | `data.text` | Yes | Text body for the post. | | Query posts with mixed structure | `dataTypes` | Yes | Content type filter, such as `image`. | | Query posts with mixed structure | `includeMixedStructure` | No | Include mixed-structure posts alongside the requested media type. | | Subscribe to post/comment events | Community or post object | Yes | Object used to build the realtime topic for the scope being observed. | | Subscribe to post/comment events | Subscription level | Yes | Realtime level, such as post-and-comment or comment-only. | ## Create a Text Post Use the text post creation page for full platform coverage; this TypeScript example shows the minimum shape for a community text post. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: createdPost } = await PostRepository.createPost({ targetType: "community", targetId: communityId, data: { text: "Hello community", }, }); ``` ## Post Structure Media posts use a parent-child structure. The parent post carries the target, text, metadata, counters, and other feed-level fields. Uploaded image, video, file, audio, or clip attachments are represented as child posts. ```mermaid graph TD A["Parent post"] --> B["Text and metadata"] A --> C["Child post: image"] A --> D["Child post: video"] A --> E["Child post: file"] A --> F["Comments"] A --> G["Reactions"] ``` Common post fields include: | Field | Notes | | --- | --- | | `postId` | Unique post ID. | | `parentPostId` | Parent ID for a child post; empty or null for parent posts depending on platform. | | `targetId` / `targetID` | Feed target ID, such as a community ID or user ID. TypeScript and iOS use `targetId`; some platform models expose differently cased names. | | `targetType` | Target type, commonly `community` or `user`. | | `dataType` / `type` | Content type such as `text`, `image`, `video`, `file`, `poll`, or custom type. Flutter exposes this as `type`. | | `structureType` | Composition type exposed by TypeScript, iOS, and Android. Flutter's current public post model does not expose this field. | | `data` | Type-specific post data. | | `metadata` | App-defined metadata. | | `commentsCount` | Server comment count at fetch time. | | `localCommentCount` | Locally computed live count exposed by TypeScript, iOS, and Android. | | `childrenPosts` / `children` | Child posts for media attachments. Field name varies by SDK. | | `isDeleted` | Soft-delete state. | ## Structure Type `structureType` classifies a post by its attachment composition. | Platform | Current behavior | | --- | --- | | TypeScript | Typed values are `text`, `image`, `video`, `file`, `audio`, and `mixed`. | | iOS | Exposes `structureType` as a string on `AmityPost`. | | Android | Exposes `getStructureType()` with values including `TEXT`, `IMAGE`, `VIDEO`, `AUDIO`, `FILE`, `LIVESTREAM`, `POLL`, `CLIP`, `ROOM`, `MIXED`, and `UNKNOWN`. | | Flutter | No public `structureType` field was found in the current public `AmityPost` model. | Use `includeMixedStructure` when querying a single media type and you also want posts whose `structureType` is `mixed`. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const stopObserving = PostRepository.getPosts( { targetType: "community", targetId: communityId, dataTypes: ["image"], includeMixedStructure: true, }, ({ data }) => { renderResults(data); } ); ``` ## Live Comment Count `localCommentCount` is a client-side count that starts from the server `commentsCount` value and then updates as the SDK observes comment create/delete events. It is exposed by TypeScript, iOS, and Android in the current SDKs. Use it for active feed or detail screens where a live counter matters. Use `commentsCount` when you only need the server value returned with the fetched post. To receive remote updates, the app must both observe the post or feed and subscribe to an appropriate realtime topic. ```typescript TypeScript import { getCommunityTopic, getPostTopic, SubscriptionLevels } from "@amityco/ts-sdk"; const communityTopic = getCommunityTopic( community, SubscriptionLevels.POST_AND_COMMENT ); const postCommentTopic = getPostTopic(post, SubscriptionLevels.COMMENT); ``` Global feed queries do not provide a single global post/comment realtime topic. Subscribe at a community, user, or post scope when the UI needs remote events. ## Related Topics Start with the simplest post creation path. Load feeds and filter by type, target, review status, or mixed structure. Track post views and meaningful views where supported. --- ### [Text Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/creation/text-post) > Create text posts with the current Social+ SDKs, including optional structured links where supported. Text posts publish written content to a user feed, a community feed, or the current user's own feed. Use them for plain updates, status messages, and posts that attach structured link metadata. Publish to user feeds, community feeds, or the current user's feed depending on the SDK target API. TypeScript, Android, and iOS can attach structured links during text post creation. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `text` | Yes | Text content for the post body. | | `targetType` | Yes for explicit feed targets | Feed target, usually `community` or `user`. | | `targetId` | Yes for explicit feed targets | Community ID or user ID for the target feed. | | `metadata` | No | Custom metadata stored with the post where supported. | | `mentionees` | No | User mention payload where supported by the platform builder. | | `links` | No | Structured link metadata supported by TypeScript, Android, and iOS. | ## Create a Text Post The examples below create a text post in a community. Use the equivalent user target method or target type when posting to a user feed. ```swift iOS let postRepository = AmityPostRepository() let builder = AmityTextPostBuilder() builder.setText("Hello community") let post = try await postRepository.createTextPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil ) ``` ```kotlin Android postRepository.createTextPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, text = "Hello community" ) .subscribe({ post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) }) ``` ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: post } = await PostRepository.createPost({ targetType: "community", targetId: communityId, data: { text: "Hello community", }, }); ``` ```dart Flutter final post = await AmitySocialClient.newPostRepository() .createPost() .targetCommunity(communityId) .text('Hello community') .createTextPost(); ``` ## Add Structured Links TypeScript, Android, and iOS expose a `links` field when creating a text post. Fetch preview metadata first if the post should render a preview card. Flutter's current public post creation builder supports text, metadata, and user mentions, but it does not expose a structured `links` payload on the text post builder. ```swift iOS let postRepository = AmityPostRepository() let builder = AmityTextPostBuilder() builder.setText("Check this out https://www.amity.co") let preview = try await client.getLinkPreviewMetadata(url: "https://www.amity.co") let links = [ AmityLink( url: "https://www.amity.co", renderPreview: true, domain: preview.domain, title: preview.title, imageUrl: preview.imageUrl ) ] let post = try await postRepository.createTextPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil, links: links ) ``` ```kotlin Android AmityCoreClient.getLinkPreviewMetadata("https://www.amity.co") .flatMap { preview -> val links = listOf( AmityLink( index = null, length = null, url = "https://www.amity.co", renderPreview = true, domain = preview.getDomain(), title = preview.getTitle(), imageUrl = preview.getImageUrl() ) ) postRepository.createTextPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, text = "Check this out https://www.amity.co", links = links ) } .subscribe({ post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) }) ``` ```typescript TypeScript import { Client, PostRepository } from "@amityco/ts-sdk"; const preview = await Client.getLinkPreviewMetadata("https://www.amity.co"); const { data: post } = await PostRepository.createPost({ targetType: "community", targetId: communityId, data: { text: "Check this out https://www.amity.co", }, links: [ { url: "https://www.amity.co", renderPreview: true, domain: preview.domain ?? undefined, title: preview.title ?? undefined, imageUrl: preview.imageUrl ?? undefined, }, ], }); ``` ## Query Text Posts Post query APIs are SDK-specific. For TypeScript, `PostRepository.getPosts` returns a live collection through a callback instead of a promise, so do not call it with `await`. For structure type values and filtering behavior, see [Posts Overview](../overview) and [Mixed Media Posts](./mixed-media-post). ## Related Topics Review post concepts, retrieval, and moderation flows. Load text posts back into feed and detail screens. Add user mention payloads where the platform exposes them. --- ### [Image Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/creation/image-post) > Create image posts with uploaded image files using the current Social+ SDKs. Image posts combine one or more uploaded images with an optional text caption. Upload the images first, then pass the uploaded image objects or file IDs to the post creation API. This page focuses on the SDK call that creates the post. See [Image Handling](/social-plus-sdk/core-concepts/content-handling/files-images-and-videos/image-handling) for upload steps, supported file inputs, and upload constraints. Create posts from image objects or file IDs returned by the file upload flow. Add text, metadata, mentions, or other SDK-supported post fields alongside the images. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `images`, `uploadedImages`, or `fileId` | Yes | Uploaded image objects or image file IDs, depending on SDK. | | `text` | No | Caption text shown with the image post. | | `targetType` | Yes | Feed target, usually `community` or `user`. | | `targetId` | Yes | Community ID or user ID for the target feed. | | `metadata` | No | Custom metadata stored with the post where supported. | | `mentionees` | No | User mention payload where supported by the platform builder. | ## Create an Image Post The examples below create an image post in a community. Replace the target with the SDK's user-feed target when posting to a user. ```swift iOS func createImagePost( uploadedImages: [AmityImageData], communityId: String ) async throws -> AmityPost { let postRepository = AmityPostRepository() let builder = AmityImagePostBuilder() builder.setImages(uploadedImages) builder.setText("Photos from today") return try await postRepository.createImagePost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil ) } ``` ```kotlin Android fun createImagePost( uploadedImages: Set, communityId: String ) { postRepository.createImagePost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, images = uploadedImages, text = "Photos from today" ) .subscribe({ post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) }) } ``` ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: post } = await PostRepository.createPost({ targetType: "community", targetId: communityId, data: { text: "Photos from today", }, attachments: [ { type: "image", fileId, }, ], }); ``` ```dart Flutter Future createImagePost(List uploadedImages) { return AmitySocialClient.newPostRepository() .createPost() .targetCommunity(communityId) .image(uploadedImages) .text('Photos from today') .post(); } ``` ## Notes - TypeScript accepts image attachments as `{ type: "image", fileId }`. - Android accepts a `Set`. - iOS accepts `[AmityImageData]`. - Flutter accepts `List`. For mixed attachment types, use [Mixed Media Posts](./mixed-media-post). ## Related Topics Upload images before creating image posts. Combine images with other supported media attachments. Review post concepts, retrieval, and moderation flows. --- ### [Video Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/creation/video-post) > Create video posts with uploaded video files using the current Social+ SDKs. Video posts combine uploaded videos with an optional text caption. Upload the videos first, then pass the uploaded video objects or file IDs to the post creation API. This page focuses on the SDK call that creates the post. See [Video Handling](/social-plus-sdk/core-concepts/content-handling/files-images-and-videos/video-handling) for upload steps, supported file inputs, processing behavior, and upload constraints. Create posts from video objects or file IDs returned by the file upload flow. Add text, metadata, mentions, or other SDK-supported post fields alongside the videos. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `videos`, `uploadedVideos`, or `fileId` | Yes | Uploaded video objects or video file IDs, depending on SDK. | | `text` | No | Caption text shown with the video post. | | `targetType` | Yes | Feed target, usually `community` or `user`. | | `targetId` | Yes | Community ID or user ID for the target feed. | | `metadata` | No | Custom metadata stored with the post where supported. | | `mentionees` | No | User mention payload where supported by the platform builder. | ## Create a Video Post The examples below create a video post in a community. Replace the target with the SDK's user-feed target when posting to a user. ```swift iOS func createVideoPost( uploadedVideos: [AmityVideoData], communityId: String ) async throws -> AmityPost { let postRepository = AmityPostRepository() let builder = AmityVideoPostBuilder() builder.setVideos(uploadedVideos) builder.setText("Video from today") return try await postRepository.createVideoPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil ) } ``` ```kotlin Android fun createVideoPost( uploadedVideos: Set, communityId: String ) { postRepository.createVideoPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, videos = uploadedVideos, text = "Video from today" ) .subscribe({ post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) }) } ``` ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: post } = await PostRepository.createPost({ targetType: "community", targetId: communityId, data: { text: "Video from today", }, attachments: [ { type: "video", fileId, }, ], }); ``` ```dart Flutter Future createVideoPost(List uploadedVideos) { return AmitySocialClient.newPostRepository() .createPost() .targetCommunity(communityId) .video(uploadedVideos) .text('Video from today') .post(); } ``` ## Notes - TypeScript accepts video attachments as `{ type: "video", fileId }`. - Android accepts a `Set`. - iOS accepts `[AmityVideoData]`. - Flutter accepts `List`. For mixed attachment types, use [Mixed Media Posts](./mixed-media-post). ## Related Topics Upload videos before creating video posts. Create short-form clip posts where supported. Combine videos with other supported media attachments. --- ### [Audio Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/creation/audio-post) > Create audio posts where the SDK exposes audio post creation. Audio posts attach uploaded audio files to a post. Upload the audio first, then pass the uploaded audio objects or file IDs to the post creation API. TypeScript, Android, and iOS expose audio post creation APIs. Flutter can upload audio files, but the current public Flutter post creation builder does not expose an audio post creation method. Create posts from audio objects or file IDs returned by the audio upload flow. Use audio post creation only on SDKs that expose it in the public post builder. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `audios`, `uploadedAudios`, or `fileId` | Yes | Uploaded audio objects or audio file IDs, depending on SDK. | | `text` | No | Caption text shown with the audio post. | | `targetType` | Yes | Feed target, usually `community` or `user`. | | `targetId` | Yes | Community ID or user ID for the target feed. | | `metadata` | No | Custom metadata stored with the post where supported. | | `mentionees` | No | User mention payload where supported by the platform builder. | ## Create an Audio Post The examples below create an audio post in a community on SDKs that support audio post creation. ```swift iOS func createAudioPost( uploadedAudios: [AmityAudioData], communityId: String ) async throws -> AmityPost { let postRepository = AmityPostRepository() let builder = AmityAudioPostBuilder() builder.setAudios(uploadedAudios) builder.setText("Listen to this") return try await postRepository.createAudioPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil ) } ``` ```kotlin Android fun createAudioPost( uploadedAudios: Set, communityId: String ) { postRepository.createAudioPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, audios = uploadedAudios, text = "Listen to this" ) .subscribe({ post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) }) } ``` ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: post } = await PostRepository.createAudioPost({ targetType: "community", targetId: communityId, data: { text: "Listen to this", }, attachments: [ { type: "audio", fileId, }, ], }); ``` ## Flutter Support Flutter's public file repository supports audio upload, but the public post creation builder currently exposes text, image, video, file, poll, live stream, and custom post creation paths. It does not expose `.audio(...)` or a dedicated audio post creator. Use a supported Flutter post type, or create audio posts from a platform/backend layer that exposes audio post creation. ## Notes - TypeScript accepts audio attachments as `{ type: "audio", fileId }`. - Android accepts a `Set`. - iOS accepts `[AmityAudioData]`. - Do not set `structureType` manually; the service derives post structure from the created post data. ## Related Topics Upload files before creating attachment-based posts. Combine multiple supported media attachment types. Review post concepts, retrieval, and moderation flows. --- ### [File Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/creation/file-post) > Create file posts with uploaded files using the current Social+ SDKs. File posts attach uploaded files to a post. Upload files first, then pass the uploaded file objects or file IDs to the post creation API. This page focuses on the SDK call that creates the post. See [File Handling](/social-plus-sdk/core-concepts/content-handling/files-images-and-videos/file) for upload steps, supported file inputs, and upload constraints. Create posts from file objects or file IDs returned by the file upload flow. Add text, metadata, mentions, or other SDK-supported post fields alongside the file attachments. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `files`, `uploadedFiles`, or `fileId` | Yes | Uploaded file objects or file IDs, depending on SDK. | | `text` | No | Caption text shown with the file post. | | `targetType` | Yes | Feed target, usually `community` or `user`. | | `targetId` | Yes | Community ID or user ID for the target feed. | | `metadata` | No | Custom metadata stored with the post where supported. | | `mentionees` | No | User mention payload where supported by the platform builder. | ## Create a File Post The examples below create a file post in a community. Replace the target with the SDK's user-feed target when posting to a user. ```swift iOS func createFilePost( uploadedFiles: [AmityFileData], communityId: String ) async throws -> AmityPost { let postRepository = AmityPostRepository() let builder = AmityFilePostBuilder() builder.setFiles(uploadedFiles) builder.setText("Resources for the team") return try await postRepository.createFilePost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil ) } ``` ```kotlin Android fun createFilePost( uploadedFiles: Set, communityId: String ) { postRepository.createFilePost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, files = uploadedFiles, text = "Resources for the team" ) .subscribe({ post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) }) } ``` ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: post } = await PostRepository.createPost({ targetType: "community", targetId: communityId, data: { text: "Resources for the team", }, attachments: [ { type: "file", fileId, }, ], }); ``` ```dart Flutter Future createFilePost(List uploadedFiles) { return AmitySocialClient.newPostRepository() .createPost() .targetCommunity(communityId) .file(uploadedFiles) .text('Resources for the team') .post(); } ``` ## Notes - TypeScript accepts file attachments as `{ type: "file", fileId }`. - Android accepts a `Set`. - iOS accepts `[AmityFileData]`. - Flutter accepts `List`. For mixed attachment types, use [Mixed Media Posts](./mixed-media-post). ## Related Topics Upload files before creating file posts. Combine files with other supported media attachments. Review post concepts, retrieval, and moderation flows. --- ### [Custom Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/creation/custom-post) > Create custom post types with application-defined structured data. Custom posts let your app publish structured content that it renders itself, such as product cards, event cards, listings, or other domain-specific content. Use a stable, application-owned data type such as `post.product` or `event.session`. Avoid built-in post data types such as `text`, `image`, `video`, `file`, `poll`, and `liveStream`. Store an application-defined JSON payload in the post data. Render custom post types with your own UI in feeds and detail screens. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `dataType` or `customDataType` | Yes | Stable application-owned type string that identifies how your app should render the post. | | `data` | Yes | JSON-style payload for the custom post. Keep this schema owned by your application. | | `targetType` | Yes | Feed target, usually `community` or `user`. | | `targetId` | Yes | Community ID or user ID for the target feed. | | `metadata` | No | Auxiliary custom metadata that should not define the post type. | | `mentionees` | No | User mention payload where supported by the platform builder. | ## Create a Custom Post The examples below create a custom product-style post in a community. Replace the data type and payload with your app's schema. ```swift iOS let postRepository = AmityPostRepository() let builder = AmityCustomPostBuilder() builder.setDataType("post.product") builder.setData([ "title": "Wireless headphones", "price": 149.99, "currency": "USD" ]) let post = try await postRepository.createCustomPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil ) ``` ```kotlin Android val data = JsonObject().apply { addProperty("title", "Wireless headphones") addProperty("price", 149.99) addProperty("currency", "USD") } postRepository.createCustomPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, customDataType = "post.product", data = data ) .subscribe({ post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) }) ``` ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: post } = await PostRepository.createPost({ targetType: "community", targetId: communityId, dataType: "post.product", data: { title: "Wireless headphones", price: 149.99, currency: "USD", }, }); ``` ```dart Flutter final post = await AmitySocialClient.newPostRepository() .createPost() .targetCommunity(communityId) .custom('post.product', { 'title': 'Wireless headphones', 'price': 149.99, 'currency': 'USD', }) .post(); ``` ## Notes - The SDK stores the custom payload; your application owns validation and rendering for the custom schema. - Use one stable data type per custom schema so feeds can route rendering predictably. - Keep custom post data focused on renderable content. Use `metadata` for auxiliary app data that should not define the post type. ## Related Topics Review post concepts, retrieval, and moderation flows. Load custom posts back into feed and detail screens. Update supported post fields after creation. --- ### [Poll Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/creation/poll-post) > Create poll posts that reference an existing Social+ poll. Poll posts share an existing poll in a user or community feed. Create the poll first with the Poll Repository, then create a post that references the returned `pollId`. Create the poll before creating the post. See [Poll Creation Guidelines](/social-plus-sdk/core-concepts/content-handling/poll#create-a-poll) for poll creation examples. A poll post points to a `pollId` returned by the poll creation flow. Publish the poll into a user feed or community feed with optional text. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `pollId` | Yes | ID of an existing poll created before post creation. | | `text` | No | Feed caption or prompt shown with the poll post. | | `targetType` | Yes | Feed target, usually `community` or `user`. | | `targetId` | Yes | Community ID or user ID for the target feed. | | `metadata` | No | Custom metadata stored with the post where supported. | | `mentionees` | No | User mention payload where supported by the platform builder. | ## Create a Poll Post The examples below create a poll post in a community from an existing `pollId`. ```swift iOS func createPollPost( pollId: String, communityId: String ) async throws -> AmityPost { let postRepository = AmityPostRepository() let builder = AmityPollPostBuilder() builder.setPollId(pollId) builder.setText("Vote in this poll") return try await postRepository.createPollPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil ) } ``` ```kotlin Android postRepository.createPollPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, pollId = pollId, text = "Vote in this poll" ) .subscribe({ post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) }) ``` ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: post } = await PostRepository.createPost({ targetType: "community", targetId: communityId, dataType: "poll", data: { text: "Vote in this poll", pollId, }, }); ``` ```dart Flutter final post = await AmitySocialClient.newPostRepository() .createPost() .targetCommunity(communityId) .poll(pollId) .text('Vote in this poll') .post(); ``` ## Notes - The post creation call does not create the poll. It only attaches an existing poll by ID. - Keep the poll question, answers, answer type, and close time in the poll creation flow. - Use the post text for feed context or a short prompt; the poll object remains the source of truth for answer options and voting behavior. ## Related Topics Create the poll object before publishing a poll post. Create a simple feed post without a poll attachment. Review post concepts, retrieval, and moderation flows. --- ### [Live Stream Posts (Deprecated)](https://learn.social.plus/social-plus-sdk/social/content-management/posts/creation/live-stream-post) > Create legacy live stream posts from an existing stream ID. For new live and co-host experiences, use room posts. Live stream posts are the legacy feed representation for an existing live stream. The stream itself must be created through the Video SDK first, then its `streamId` can be attached to a post. For new live and co-host room experiences, create a room and publish a room post instead. The iOS live stream post API is deprecated in favor of `createRoomPost`. ## When to Use | Use case | Recommended post type | | --- | --- | | Existing legacy live stream integration | Live stream post | | New live broadcast or co-host room experience | Room post | | Uploaded video playback | Video post or clip post | ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `streamId` | Yes | ID of the live stream created before post creation | | `text` | No | Caption text shown with the post | | `targetType` | Yes | Feed target, usually `community` or `user` | | `targetId` | Yes | Community ID or user ID for the target feed | | `metadata` | No | Custom metadata stored with the post where supported | ## Create a Live Stream Post Create a legacy live stream post only when your product still has an existing stream ID from the older live-stream flow. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: post } = await PostRepository.createPost({ targetType: "community", targetId: communityId, dataType: "liveStream", data: { streamId, text: "Watch this live session", }, }); ``` ```kotlin Android fun createLegacyLiveStreamPost(streamId: String, communityId: String) { postRepository.createLiveStreamPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, streamId = streamId, text = "Watch this live session" ) .subscribe( { post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) } ) } ``` ```swift iOS func createLegacyLiveStreamPost( streamId: String, communityId: String ) async throws -> AmityPost { let postRepository = AmityPostRepository() let builder = AmityLiveStreamPostBuilder( streamId: streamId, text: "Watch this live session" ) return try await postRepository.createLiveStreamPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil ) } ``` ```dart Flutter Future createLegacyLiveStreamPost( String streamId, String communityId, ) { return AmitySocialClient.newPostRepository() .createPost() .targetCommunity(communityId) .liveStream(streamId) .text('Watch this live session') .post(); } ``` ## Platform Notes - TypeScript creates a legacy live stream post through `PostRepository.createPost()` with `dataType: "liveStream"`. - Android exposes `AmityPostRepository.createLiveStreamPost()`. - iOS exposes `AmityPostRepository.createLiveStreamPost()`, but the method is deprecated in favor of `createRoomPost()`. - Flutter exposes `.liveStream(streamId)` on the public post creation builder. ## Related Topics Use room posts for new live and co-host experiences. Create posts from uploaded video files. Create and manage live streams before posting. --- ### [Clip Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/creation/clip-post) > Create short-form video posts from an uploaded clip. Clip posts publish an uploaded clip into a user or community feed. Upload the clip first, then pass the uploaded file ID or clip data into the post creation API. Use clip posts for short-form video experiences that need the `clip` post type. Use video posts when you only need the standard uploaded-video post flow. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `clip` or `fileId` | Yes | Uploaded clip data or clip file ID, depending on SDK | | `text` | No | Caption text shown with the post | | `displayMode` | No | Clip display mode where supported, such as `fill` or `fit` | | `isMuted` | No | Whether playback should start muted where supported | | `targetType` | Yes | Feed target, usually `community` or `user` | | `targetId` | Yes | Community ID or user ID for the target feed | ## Create a Clip Post Create a clip post from an uploaded clip file or clip data, and include optional display settings where supported. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: post } = await PostRepository.createClipPost({ targetType: "community", targetId: communityId, data: { text: "Watch this clip", }, attachments: [ { type: "clip", fileId, displayMode: "fill", isMuted: false, }, ], }); ``` ```kotlin Android fun createClipPost(uploadedClip: AmityClip, communityId: String) { postRepository.createClipPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, clip = uploadedClip, text = "Watch this clip", displayMode = AmityClip.DisplayMode.FILL, isMuted = false ) .subscribe( { post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) } ) } ``` ```swift iOS func createClipPost( uploadedClip: AmityClipData, communityId: String ) async throws -> AmityPost { let postRepository = AmityPostRepository() let builder = AmityClipPostBuilder() builder.setClip(uploadedClip) builder.setText("Watch this clip") builder.setDisplayMode(.fill) builder.setIsMuted(false) return try await postRepository.createClipPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil ) } ``` The current Flutter public post creation builder does not expose a clip post creator. It supports text, image, video, file, poll, live stream, and custom post creation. ## Platform Notes - TypeScript exposes `PostRepository.createClipPost()` with `attachments: [{ type: "clip", fileId }]`. - Android exposes `AmityPostRepository.createClipPost()` with an uploaded `AmityClip`. - iOS exposes `AmityPostRepository.createClipPost()` with `AmityClipPostBuilder` and uploaded `AmityClipData`. - Flutter does not currently expose a public clip post creation method. ## Related Topics Upload video files before creating video-based posts. Create standard uploaded-video posts. Combine multiple supported media attachments in one post. --- ### [Mixed Media Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/creation/mixed-media-post) > Create posts that combine multiple uploaded media attachment types where the SDK exposes mixed media creation. Mixed media posts combine two or more uploaded attachment types in one post, such as images with videos or files with audio. Upload each media item first, then pass the uploaded media objects or file IDs to the mixed media creation API. TypeScript, Android, and iOS expose mixed media post creation APIs. The current public Flutter post creation builder exposes single attachment-type creators for image, video, and file posts, but it does not expose a mixed media post creator. Keep related uploaded media in one post instead of splitting it into separate posts. The service derives the post structure from the attachment types; do not set it manually. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `attachments`, uploaded media sets, or file IDs | Yes | Uploaded media items to combine in one post. Supported attachment types depend on SDK. | | `text` | No | Caption text shown with the mixed media post. | | `targetType` | Yes | Feed target, usually `community` or `user`. | | `targetId` | Yes | Community ID or user ID for the target feed. | | `metadata` | No | Custom metadata stored with the post where supported. | | `mentionees` | No | User mention payload where supported by the platform builder. | ## Create a Mixed Media Post The examples below combine image and video attachments in a community post. Add audio or file attachments on SDKs that expose those uploaded media object types. ```swift iOS func createMixedMediaPost( uploadedImages: [AmityImageData], uploadedVideos: [AmityVideoData], communityId: String ) async throws -> AmityPost { let postRepository = AmityPostRepository() let builder = AmityMixedMediaPostBuilder() builder.setText("Photos and video from today") builder.setImages(uploadedImages) builder.setVideos(uploadedVideos) return try await postRepository.createMixedMediaPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil ) } ``` ```kotlin Android fun createMixedMediaPost( uploadedImages: Set, uploadedVideos: Set, communityId: String ) { val attachments = mutableSetOf() attachments.addAll(uploadedImages) attachments.addAll(uploadedVideos) postRepository.createMixedAttachmentPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, attachments = attachments, text = "Photos and video from today" ) .subscribe({ post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) }) } ``` ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: post } = await PostRepository.createPost({ targetType: "community", targetId: communityId, data: { text: "Photos and video from today", }, attachments: [ { type: "image", fileId: imageFileId, }, { type: "video", fileId: videoFileId, }, ], }); ``` ## Flutter Support Flutter's current public post creation builder exposes `.image(...)`, `.video(...)`, and `.file(...)` for single attachment-type posts. It does not expose a public mixed media builder that accepts multiple attachment types in one post creation call. Create a single supported post type from Flutter, or create mixed media posts from a platform/backend layer that exposes mixed attachment creation. ## Notes - TypeScript accepts mixed attachments in `PostRepository.createPost`. - Android accepts a `Set` through `createMixedAttachmentPost`. - iOS accepts uploaded media through `AmityMixedMediaPostBuilder`. - Do not pass `structureType` in the create request. The service derives it from the attachment composition. ## Related Topics Create image-only posts from uploaded images. Create video-only posts from uploaded videos. Create file-only posts from uploaded files. --- ### [Room Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/creation/room-post) > Create feed posts that reference an existing live room. Room posts publish an existing room into a user or community feed. Create the room first through the room or video APIs, then pass its `roomId` to the post creation API. Room posts are the current path for new live and co-host room experiences. This page focuses only on creating the feed post from an existing `roomId`. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `roomId` | Yes | ID of the room to attach to the post | | `text` | No | Caption text shown with the post | | `title` | No | Optional title stored in the room post data | | `targetType` | Yes | Feed target, usually `community` or `user` | | `targetId` | Yes | Community ID or user ID for the target feed | | `metadata` | No | Custom metadata stored with the post where supported | ## Create a Room Post Create a room post after the room already exists, then use the returned post in the target user or community feed. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: post } = await PostRepository.createRoomPost({ targetType: "community", targetId: communityId, data: { roomId, text: "Join this live room", title: "Live room", }, }); ``` ```kotlin Android fun createRoomPost(roomId: String, communityId: String) { postRepository.createRoomPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, roomId = roomId, text = "Join this live room", title = "Live room" ) .subscribe( { post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) } ) } ``` ```swift iOS func createRoomPost(roomId: String, communityId: String) async throws -> AmityPost { let postRepository = AmityPostRepository() let builder = AmityRoomPostBuilder( roomId: roomId, text: "Join this live room" ) builder.setTitle("Live room") return try await postRepository.createRoomPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil ) } ``` The current Flutter public post creation builder does not expose a room post creator. Use another supported SDK or backend flow when your product needs to publish room posts from Flutter. ## Platform Notes - TypeScript exposes `PostRepository.createRoomPost()`. - Android exposes `AmityPostRepository.createRoomPost()`. - iOS exposes `AmityPostRepository.createRoomPost()` with `AmityRoomPostBuilder`. - Flutter does not currently expose a public room post creation method. ## Related Topics Review the deprecated legacy live stream post path. Create posts from uploaded video files. Review posts, comments, reactions, and sharing concepts. --- ### [Get Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/retrieval/get-post) > Retrieve one post as a live object, or fetch a known set of post IDs where the SDK supports batch lookup. Use post retrieval when your app already knows the post ID. Single-post retrieval returns a live object or stream on every SDK. Batch lookup is available on TypeScript, iOS, and Android. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Single post | `postId` | Yes | Unique post ID to observe or fetch. | | Multiple posts by ID | `postIds` | Yes | List or set of post IDs to fetch on TypeScript, iOS, and Android. | ## Single Post Observe a single post when a detail screen should update as post data, counters, or moderation state changes. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const unsubscribe = PostRepository.getPost( postId, ({ data: post, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } if (post) { renderResults(post); } }, ); ``` ```kotlin Android postRepository.getPost(postId) .subscribe( { post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS let postRepository = AmityPostRepository() var token: AmityNotificationToken? token = postRepository.getPost(withId: "post-id").observe { liveObject, error in guard let post = liveObject.snapshot else { return } showSuccessMessage(post.postId) } ``` ```dart Flutter final subscription = AmitySocialClient.newPostRepository() .live .getPost(postId) .listen((AmityPost post) { final id = post.postId; }); await subscription.cancel(); ``` ## Multiple Posts by ID Fetch a known set of post IDs when your app already has exact IDs and does not need a paged feed query. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: posts } = await PostRepository.getPostByIds([ postId, "another-post-id", ]); ``` ```kotlin Android postRepository.getPostByIds(setOf(postId, "another-post-id")) .subscribe( { posts -> showSuccessMessage(posts.size) }, { error -> handleGeneralError(error) } ) ``` ```swift iOS let postRepository = AmityPostRepository() var token: AmityNotificationToken? token = postRepository.getPosts(postIds: ["post-id", "another-post-id"]).observe { collection, error in showSuccessMessage(collection.snapshots.count) } ``` The current Flutter public post repository exposes single-post retrieval through `live.getPost(postId)` and the deprecated `getPost(postId)` future, but it does not expose a public batch get-by-IDs method. ## Notes - Keep the unsubscriber or notification token so you can dispose the live object when the screen is destroyed. - Use query APIs when you need a feed, pagination, filtering, or real-time collection updates. - Batch lookup skips invalid IDs on iOS collection results; handle an empty collection as a valid outcome. ## Related Topics Query posts by feed target, post type, review status, or tags. Render post data and child media safely. Review post structure and post data types. --- ### [Query Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/retrieval/query-posts) > Query live post collections by target, data type, review status, deletion state, tags, and pagination options. Use post queries to build user feeds, community feeds, review queues, and media galleries. Query results are paginated live collections on the client SDKs. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Query posts | Target filter | Context-dependent | Target type and target ID for user or community feeds. | | Query posts | Data type filter | No | Post content types to include, such as image, video, file, poll, or custom. | | Query posts | Deleted-state filter | No | Include or exclude deleted posts where supported. | | Query posts | Review/feed status | No | Published, reviewing, or declined state filters where exposed by the SDK. | | Query posts | `tags` | No | Tags to match when building tag-filtered feeds. | | Query posts | `includeMixedStructure` | No | Include mixed-structure posts alongside media-type filters where supported. | | Query posts | `untilAt` | No | Time boundary for pagination where supported. | | Android cache invalidation | `invalidateCache` | No | Android-only option to skip stale paging cache for a query. | ## Common Filters | Filter | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Target | `targetType`, `targetId` | `targetType`, `targetId` | `.targetUser()`, `.targetCommunity()` | `.targetUser()`, `.targetCommunity()` | | Data type | `dataTypes` | `dataTypes` | `.dataTypes()` | `.types()` | | Include deleted | `includeDeleted` | `deletedOption` | `.includeDeleted()` | `.includeDeleted()` | | Review/feed status | `feedType` with published or reviewing | `feedType` | `.reviewStatus()` | `.feedType()` | | Tags | `tags` | `tags` | `.tags()` | `.tags()` | | Mixed media matching | `includeMixedStructure` | `includeMixedStructure` | `.includeMixedStructure()` | Not exposed in the current public builder | | Time boundary | `untilAt` | `untilAt` | `.untilAt()` | Not exposed in the current public builder | | Invalidate cache | Not exposed | Not exposed | `.invalidateCache()` | Not exposed | ## Data Type Filters | Platform | Query data type values | | --- | --- | | TypeScript | Non-text post data types through `dataTypes`, including image, video, file, poll, live stream, audio, clip, room, and custom strings | | iOS | String `dataTypes`; deprecated `filterPostTypes` exists but new docs should use `dataTypes` | | Android | `AmityPost.DataType`, including text, image, video, file, poll, live stream, audio, clip, room, and custom | | Flutter | `AmityDataType.TEXT`, `IMAGE`, `VIDEO`, `FILE`, `LIVESTREAM`, `POLL`, and `CUSTOM`; no public audio, clip, or room enum values | ## Query a Community Feed Query a community feed with the filters your UI needs, then keep the returned live collection or stream subscription while the feed is visible. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; let loadNextPage: (() => void) | undefined; let canLoadMore = false; const unsubscribe = PostRepository.getPosts( { targetType: "community", targetId: communityId, dataTypes: ["image", "video", "poll"], includeDeleted: false, feedType: "published", sortBy: "lastCreated", tags: ["product", "promotion"], includeMixedStructure: true, }, ({ data: posts, onNextPage, hasNextPage, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(posts); loadNextPage = onNextPage; canLoadMore = hasNextPage; }, ); function loadMorePosts() { if (canLoadMore) { loadNextPage?.(); } } ``` ```swift iOS let postRepository = AmityPostRepository() var token: AmityNotificationToken? let options = AmityPostQueryOptions( targetType: .community, targetId: communityId, sortBy: .lastCreated, deletedOption: .notDeleted, dataTypes: Set(["image", "video", "poll"]), feedType: .published, tags: ["product", "promotion"], includeMixedStructure: true ) token = postRepository.getPosts(options).observe { collection, error in showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android postRepository .getPosts() .targetCommunity(communityId = communityId) .dataTypes( dataTypes = listOf( AmityPost.DataType.IMAGE, AmityPost.DataType.VIDEO, AmityPost.DataType.POLL ) ) .includeDeleted(includeDeleted = false) .reviewStatus(AmityReviewStatus.PUBLISHED) .tags(tags = listOf("product", "promotion")) .includeMixedStructure(includeMixedStructure = true) .sortBy(sortOption = AmityCommunityFeedSortOption.LAST_CREATED) .build() .query() .subscribe( { pagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final postLiveCollection = AmitySocialClient.newPostRepository() .getPosts() .targetCommunity(communityId) .types([ AmityDataType.IMAGE, AmityDataType.VIDEO, AmityDataType.POLL, ]) .includeDeleted(false) .feedType(AmityFeedType.PUBLISHED) .tags(['product', 'promotion']) .getLiveCollection(pageSize: 20); postLiveCollection.getStreamController().stream.listen((posts) { final count = posts.length; }); await postLiveCollection.loadNext(); if (postLiveCollection.hasNextPage()) { await postLiveCollection.loadNext(); } ``` ## Android Cache Invalidation Android exposes `invalidateCache(true)` on post query builders. Use it when a screen should skip stale paging cache on entry, such as after pull-to-refresh. ```kotlin Android postRepository .getPosts() .targetCommunity(communityId = communityId) .includeDeleted(includeDeleted = false) .invalidateCache(invalidateCache = true) .build() .query() .subscribe( { pagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ## Time Boundaries `untilAt` is available on TypeScript, iOS, and Android query surfaces. It is a time boundary for pagination. For newest-first sorting, older posts beyond the boundary are excluded; for oldest-first sorting, newer posts beyond the boundary are excluded. ```kotlin Android val oneWeekAgo = DateTime.now().minusDays(7) postRepository .getPosts() .targetCommunity(communityId = communityId) .untilAt(oneWeekAgo) .build() .query() .subscribe( { pagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ## Notes - For media galleries, set a data type filter and enable mixed media matching where the SDK supports it. - For moderation review queues, use review/feed status filters and apply your app's permission checks before showing restricted states. - Always dispose live collection subscriptions, notification tokens, or stream subscriptions when the screen is destroyed. ## Related Topics Retrieve one known post or a known set of post IDs. Render returned post content by type. Review approval and declined-post flows. --- ### [Viewing Post Content](https://learn.social.plus/social-plus-sdk/social/content-management/posts/retrieval/viewing-content) > Render post text, child media, polls, live stream data, room data, and local counts from returned SDK post objects. Posts can contain text directly and can also reference child posts for media attachments. Query and get APIs return composed post objects where each SDK exposes helpers for reading the post data. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Inspect post content | Post object | Yes | Post returned from a get or query API. Use SDK helper methods or typed data accessors on that object. | ## Parent and Child Posts Media posts commonly use a parent-child shape: - The parent post carries the main feed item and text. - Child posts carry individual media attachments such as images, videos, and files. - SDK helpers expose composed children where the platform supports them. ```typescript TypeScript function inspectPostContent(post: Amity.Post) { const childCount = post.childrenPosts.length; const localCommentCount = post.localCommentCount; const image = post.getImageInfo(); const video = post.getVideoInfo(); const videoThumbnail = post.getVideoThumbnailInfo(); const file = post.getFileInfo(); const poll = post.getPollInfo(); const audio = post.getAudioInfo(); const clip = post.getClipInfo(); const room = post.getRoomInfo(); renderResults({ childCount, localCommentCount, image, video, videoThumbnail, file, poll, audio, clip, room, }); } ``` ```swift iOS func inspectPostContent(_ post: AmityPost) { let childCount = post.childrenPosts.count let localCommentCount = post.localCommentCount let image = post.getImageInfo() let video = post.getVideoInfo() let videoThumbnail = post.getVideoThumbnailInfo() let file = post.getFileInfo() let poll = post.getPollInfo() let audio = post.getAudioInfo() let clip = post.getClipInfo() let room = post.getRoomInfo() showSuccessMessage([ childCount, localCommentCount, image?.fileId, video?.fileId, videoThumbnail?.fileId, file?.fileId, poll?.pollId, audio?.fileId, clip?.fileId, room?.roomId ]) } ``` ```kotlin Android fun inspectPostContent(post: AmityPost) { val children = post.getChildren() val localCommentCount = post.getLocalCommentCount() when (val data = post.getData()) { is AmityPost.Data.TEXT -> showSuccessMessage(data.getText()) is AmityPost.Data.IMAGE -> showSuccessMessage(data.getImage() ?: "") is AmityPost.Data.FILE -> showSuccessMessage(data.getFile() ?: "") is AmityPost.Data.AUDIO -> showSuccessMessage(data.getAudio() ?: "") is AmityPost.Data.VIDEO -> showSuccessMessage(data.getThumbnailImage() ?: "") is AmityPost.Data.CLIP -> showSuccessMessage(data.getThumbnailImage() ?: "") is AmityPost.Data.LIVE_STREAM -> showSuccessMessage(data.getPostId()) is AmityPost.Data.ROOM -> showSuccessMessage(data.getRoom() ?: "") is AmityPost.Data.POLL -> showSuccessMessage(data.getPollId()) is AmityPost.Data.CUSTOM -> showSuccessMessage(data.getDataType()) } showSuccessMessage(children.size + localCommentCount) } ``` ```dart Flutter void inspectPostContent(AmityPost post) { final children = post.children ?? []; final commentCount = post.commentCount ?? 0; final data = post.data; if (data is TextData) { final text = data.text; } else if (data is ImageData) { final imageUrl = data.getUrl(AmityImageSize.MEDIUM); } else if (data is FileData) { final file = data.file; } else if (data is VideoData) { final thumbnail = data.thumbnail; } else if (data is LiveStreamData) { final streamId = data.streamId; } else if (data is PollData) { final pollId = data.pollId; } else if (data is CustomData) { final rawData = data.rawData; } final childCount = children.length; final total = childCount + commentCount; } ``` ## Platform Notes | Platform | Content helpers | | --- | --- | | TypeScript | Linked post helpers include `childrenPosts`, `getImageInfo()`, `getVideoInfo()`, `getVideoThumbnailInfo()`, `getFileInfo()`, `getPollInfo()`, `getAudioInfo()`, `getClipInfo()`, and `getRoomInfo()` | | iOS | `AmityPost` exposes `childrenPosts` plus helper methods for image, video, thumbnail, file, poll, audio, clip, and room data | | Android | `AmityPost.getData()` returns a sealed data type; inspect it with `when` and use data-specific methods | | Flutter | Current public model exposes text, image, video, file, live stream, poll, and custom data classes; audio, clip, and room post data classes are not exposed in the current public model | ## Rendering Guidance - Prefer SDK helper methods over reading raw data maps when helpers exist. - Handle unknown or custom data types gracefully; custom post types are application-defined. - For video and clip playback, read the media object first and choose a URL or resolution your UI can render. - For room posts, check room status before presenting live or recorded playback UI. - Use `localCommentCount` where exposed when your UI should include replies in the visible comment count. ## Related Topics Query live post collections before rendering Create posts with multiple media attachment types Create room posts for live room experiences --- ### [Edit Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/moderation/edit-post) > Update existing posts with the public SDK edit APIs and platform-specific editor surfaces. Use post editing when your app needs to update content that already exists. The SDKs update the post by ID; permission checks are enforced by the backend for post owners, moderators, and admins according to your app configuration. ## Platform Support | Platform | Public edit surface | Notes | | --- | --- | --- | | TypeScript | `PostRepository.editPost(postId, patch)` | Patch can include `data`, `metadata`, `tags`, `mentionees`, `hashtags`, `attachments`, `links`, `productTags`, and `attachmentProductTags` | | iOS | `postRepository.editPost(withId:builder:...)` | Use the builder type that matches the original post content type | | Android | `postRepository.editPost(postId).build().apply()` | Builder supports text, title, attachments, metadata, mentions, hashtags, tags, links, product tags, and tagged attachment products | | Flutter | `post.edit().build().update()` | Editor is available on an `AmityPost`; it supports text, image, file, video, custom data, metadata, and mentioned users | The current Flutter public editor does not expose link preview replacement or product-tag editing. Keep those updates on platforms that expose the fields. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Update a post | `postId` | Yes | Post ID to update. Flutter updates through a loaded `AmityPost` object. | | Update a post | Text/data patch | Usually | Updated text or post data supported by the original post type. | | Update a post | Attachments/media | No | Complete attachment set to keep where the SDK exposes attachment editing. | | Update a post | `metadata`, `tags`, mentions, links | No | Optional fields exposed by the target SDK edit surface. | ## Update a Post Load or know the post ID, then apply the platform-specific editor or patch API for the fields your product allows users to change. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: updatedPost } = await PostRepository.editPost(postId, { data: { text: "Updated caption with https://www.amity.co", }, attachments: [ { type: "image", fileId: imageFileId, }, ], metadata: { editedFrom: "profile", }, tags: ["release-note"], links: [ { url: "https://www.amity.co", index: 21, length: 20, renderPreview: true, domain: "www.amity.co", }, ], }); renderResults(updatedPost); ``` ```swift iOS let builder = AmityTextPostBuilder() builder.setText("Updated caption") let updatedPost = try await postRepository.editPost( withId: "post-id", builder: builder, metadata: ["editedFrom": "profile"], mentionees: nil, hashtags: nil, links: nil, productTags: nil, attachmentProductTags: nil ) showSuccessMessage(updatedPost.postId) ``` ```kotlin Android val metadata = JsonObject().apply { addProperty("editedFrom", "profile") } postRepository.editPost(postId = postId) .text(text = "Updated caption") .metadata(metadata) .tags(listOf("release-note")) .build() .apply() .subscribe( { showSuccessMessage(postId) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final post = await AmitySocialClient.newPostRepository().getPost(postId); await post .edit() .text('Updated caption') .metadata({'editedFrom': 'profile'}) .build() .update(); ``` ## Editing Media On platforms with attachment editing, send the complete attachment set you want the post to keep. If you omit an existing image, file, or video from the update payload, that attachment should be treated as removed from the edited post. | Platform | Media edit pattern | | --- | --- | | TypeScript | Send `attachments` with `{ type, fileId }` entries | | iOS | Rebuild the post with the matching media builder, such as `AmityImagePostBuilder`, `AmityFilePostBuilder`, or `AmityVideoPostBuilder` | | Android | Pass media objects through `.attachments(...)` on the edit builder | | Flutter | Use `.image(...)`, `.file(...)`, or `.video(...)` on the loaded post editor | ## Notes - Keep the edit operation tied to the content type that was originally created. - Preserve existing attachments by reading the current post first and including the items that should remain. - After a successful edit, use the returned or live-updated post to refresh UI state such as edited timestamps, tags, metadata, and attachment lists. ## Related Topics Load the current post before editing. Remove posts through soft or hard deletion. Render the updated post content. --- ### [Delete Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/moderation/delete-post) > Delete posts through the public SDK soft-delete and hard-delete surfaces. Use post deletion when a user or moderator needs to remove a post by ID. Deletion permissions are enforced by the backend according to the actor, target community, and your app settings. ## Platform Support | Platform | Soft delete | Hard delete | Public SDK method | | --- | --- | --- | --- | | TypeScript | Yes | Yes | `PostRepository.softDeletePost(postId)` and `PostRepository.hardDeletePost(postId)` | | iOS | Yes | Yes | `softDeletePost(withId:parentId:)` and `hardDeletePost(withId:parentId:)` | | Android | Yes | Yes | `softDeletePost(postId)` and `hardDeletePost(postId)` | | Flutter | Yes | No verified hard-delete flag | `deletePost(postId:)` or `post.delete()` | Do not rely on Flutter's `hardDelete` argument for hard deletion in the current public SDK. The public delete path does not pass a hard-delete flag to the request. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Delete a post | `postId` | Yes | Post ID to soft delete or hard delete. | | Delete a post | Delete type | Yes | Choose soft delete or hard delete where the target SDK exposes both behaviors. | | Delete a post | `parentId` | No | Optional iOS parent post ID for child-post deletion state updates. | ## Delete a Post Delete by post ID, choosing soft delete or hard delete only on SDKs that expose the desired behavior. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const softDeletedPost = await PostRepository.softDeletePost(postId); const hardDeletedPost = await PostRepository.hardDeletePost(postId); renderResults([softDeletedPost, hardDeletedPost]); ``` ```swift iOS try await postRepository.softDeletePost(withId: "post-id", parentId: nil) try await postRepository.hardDeletePost(withId: "post-id", parentId: nil) showSuccessMessage() ``` ```kotlin Android postRepository.softDeletePost(postId = postId) .subscribe( { showSuccessMessage(postId) }, { error -> handleGeneralError(error) } ) postRepository.hardDeletePost(postId = postId) .subscribe( { showSuccessMessage(postId) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter await AmitySocialClient.newPostRepository().deletePost(postId: postId); ``` ## Parent Post IDs iOS delete methods accept an optional `parentId`. Pass `nil` for top-level posts. If you are deleting a child post and your app already has its parent post ID, pass that parent ID so local post state can be updated correctly. ## Notes - Use soft delete when your app needs the deleted state to remain observable. - Use hard delete only when your app intentionally wants the object invalidated or permanently removed on SDKs that expose the hard-delete path. - Refresh or remove local UI items after deletion; live collections usually update, but optimistic UI should still handle delete failures. ## Related Topics Update existing post content. Approve or decline posts in review queues. Query posts with deleted-state filters. --- ### [Pinned Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/moderation/pin-post) > Read community and global pinned posts through the public SDK live collection APIs. Use pinned-post queries to display posts that were already pinned outside the client SDK. The SDK surfaces in this page are read-only retrieval APIs. ## Platform Support | Platform | Community pinned posts | Global pinned posts | Placement values | | --- | --- | --- | --- | | TypeScript | `PostRepository.getPinnedPosts(...)` | `PostRepository.getGlobalPinnedPosts(...)` | String placement such as `default` or `announcement`; `null` fetches all community placements | | iOS | `postRepository.getPinnedPosts(...)` | `postRepository.getGlobalPinnedPosts(...)` | `AmityPinPlacement.default.rawValue` or `AmityPinPlacement.announcement.rawValue` | | Android | `postRepository.getPinnedPosts(...)` | `postRepository.getGlobalPinnedPosts(...)` | `AmityPinnedPost.PinPlacement.DEFAULT.value` or `ANNOUNCEMENT.value` | | Flutter | `AmitySocialClient.newPostRepository().getPinnedPosts(...)` | `getGlobalPinnedPosts()` | String placement, or `PinPlacement.DEFAULT.value` / `ANNOUNCEMENT.value` | Current client SDKs retrieve pinned posts. Pinning and unpinning are not exposed as public client SDK operations in this surface. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Community pinned posts | `communityId` | Yes | Community whose pinned posts should be retrieved. | | Community pinned posts | `placement` | No | Placement such as default or announcement. | | Community pinned posts | `sortBy` | No | Sort order where exposed by the target SDK. | | Community pinned posts | `includeMixedStructure` | No | Include mixed-structure posts where supported. | | Global pinned posts | `includeMixedStructure` | No | Include mixed-structure posts in global pinned results where supported. | ## Community Pinned Posts Query community pinned posts for a specific community and optional placement such as an announcement area. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const unsubscribe = PostRepository.getPinnedPosts( { communityId, placement: "announcement", sortBy: "lastPinned", includeMixedStructure: true, }, ({ data: pinnedPosts, loading, error, onNextPage, hasNextPage }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(pinnedPosts); if (hasNextPage) { onNextPage?.(); } }, ); ``` ```swift iOS token = postRepository .getPinnedPosts( communityId: communityId, placement: AmityPinPlacement.announcement.rawValue, sortBy: .lastPinned, includeMixedStructure: true ) .observe { collection, error in showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android postRepository.getPinnedPosts( communityId = communityId, placement = AmityPinnedPost.PinPlacement.ANNOUNCEMENT.value, includeMixedStructure = true ) .subscribe( { pagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final pinnedPosts = AmitySocialClient.newPostRepository().getPinnedPosts( communityId: communityId, placement: 'announcement', ); pinnedPosts.getStreamController().stream.listen((items) { final count = items.length; }); await pinnedPosts.loadNext(); ``` ## Global Pinned Posts Query global pinned posts when the product needs an app-wide featured-content surface. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const unsubscribe = PostRepository.getGlobalPinnedPosts( { includeMixedStructure: true, }, ({ data: pinnedPosts, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(pinnedPosts); }, ); ``` ```swift iOS token = postRepository .getGlobalPinnedPosts(includeMixedStructure: true) .observe { collection, error in showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android postRepository.getGlobalPinnedPosts(includeMixedStructure = true) .subscribe( { pinnedPosts -> showSuccessMessage(pinnedPosts.size) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final globalPinnedPosts = AmitySocialClient.newPostRepository().getGlobalPinnedPosts(); globalPinnedPosts.getStreamController().stream.listen((items) { final count = items.length; }); await globalPinnedPosts.loadNext(); ``` ## Notes - Use community pinned posts for a single community feed or announcement area. - Use global pinned posts for app-wide featured content. - TypeScript, iOS, and Android expose `includeMixedStructure`; the current Flutter public pinned-post methods do not expose that option. - Flutter exposes a `sortByOptions` parameter on `getPinnedPosts`, but the current public request builder does not pass it into the request. Do not depend on custom sorting there. ## Related Topics Query regular feed posts. Render the pinned post object. Approve or decline posts. --- ### [Post Review](https://learn.social.plus/social-plus-sdk/social/content-management/posts/moderation/post-review) > Approve, decline, and query posts in community review states with the public SDKs. Use post review when a community requires posts to be approved before they appear in the published feed. The SDKs expose approval actions by post ID, plus query filters for building a review queue. ## Platform Support | Platform | Approve | Decline | Query review queue | | --- | --- | --- | --- | | TypeScript | `PostRepository.approvePost(postId)` | `PostRepository.declinePost(postId)` | `PostRepository.getPosts({ feedType: "reviewing" })` | | iOS | `postRepository.approvePost(withId:)` | `postRepository.declinePost(withId:)` | `AmityPostQueryOptions(feedType: .reviewing)` | | Android | `postRepository.approvePost(postId)` | `postRepository.declinePost(postId)` | `.reviewStatus(AmityReviewStatus.UNDER_REVIEW)` | | Flutter | `reviewPost(postId:).approve()` | `reviewPost(postId:).decline()` | `.feedType(AmityFeedType.REVIEWING)` | TypeScript's current post query type exposes `published` and `reviewing` as live collection feed filters. Android, iOS, and Flutter expose published, reviewing, and declined states in their review/feed status models. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Check review status | Post object | Yes | Loaded post object whose feed/review status should be read. | | Approve or decline | `postId` | Yes | Post ID to approve or decline. | | Query a review queue | `communityId` | Yes | Community whose reviewing posts should be listed. | | Query a review queue | Review/feed status | Yes | Reviewing/under-review filter for the target SDK. | | Query a review queue | Sort and pagination controls | No | Optional collection controls exposed by the platform. | ## Check Review Status Read review status from a loaded post before deciding which moderation actions or labels to show. ```typescript TypeScript const isPublished = post.feedType === "published"; const isUnderReview = post.feedType === "reviewing"; renderResults({ isPublished, isUnderReview }); ``` ```swift iOS if let currentPost = postRepository.getPost(withId: "post-id").snapshot { switch currentPost.getFeedType() { case .published: showSuccessMessage("published") case .reviewing: showSuccessMessage("reviewing") case .declined: showSuccessMessage("declined") @unknown default: showSuccessMessage("unknown") } } ``` ```kotlin Android when (post?.getReviewStatus()) { AmityReviewStatus.PUBLISHED -> showSuccessMessage("published") AmityReviewStatus.UNDER_REVIEW -> showSuccessMessage("reviewing") AmityReviewStatus.DECLINED -> showSuccessMessage("declined") null -> showSuccessMessage("unknown") } ``` ```dart Flutter final post = await AmitySocialClient.newPostRepository().getPost(postId); switch (post.feedType) { case AmityFeedType.PUBLISHED: final status = 'published'; break; case AmityFeedType.REVIEWING: final status = 'reviewing'; break; case AmityFeedType.DECLINED: final status = 'declined'; break; default: final status = 'unknown'; } ``` ## Approve or Decline Approve or decline by post ID after your product confirms the current user should moderate that community. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const { data: approvedPost } = await PostRepository.approvePost(postId); const { data: declinedPost } = await PostRepository.declinePost(postId); renderResults([approvedPost, declinedPost]); ``` ```swift iOS try await postRepository.approvePost(withId: "post-id") try await postRepository.declinePost(withId: "post-id") showSuccessMessage() ``` ```kotlin Android postRepository.approvePost(postId = postId) .subscribe( { showSuccessMessage(postId) }, { error -> handleGeneralError(error) } ) postRepository.declinePost(postId = postId) .subscribe( { showSuccessMessage(postId) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter await AmitySocialClient.newPostRepository() .reviewPost(postId: postId) .approve(); await AmitySocialClient.newPostRepository() .reviewPost(postId: postId) .decline(); ``` ## Query a Review Queue Query reviewing posts for a community when building a moderation queue. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const unsubscribe = PostRepository.getPosts( { targetType: "community", targetId: communityId, feedType: "reviewing", sortBy: "lastCreated", }, ({ data: posts, loading, error, onNextPage, hasNextPage }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(posts); if (hasNextPage) { onNextPage?.(); } }, ); ``` ```swift iOS let options = AmityPostQueryOptions( targetType: .community, targetId: communityId, sortBy: .lastCreated, deletedOption: .notDeleted, dataTypes: nil, feedType: .reviewing ) token = postRepository.getPosts(options).observe { collection, error in showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android postRepository .getPosts() .targetCommunity(communityId = communityId) .reviewStatus(reviewStatus = AmityReviewStatus.UNDER_REVIEW) .build() .query() .subscribe( { pagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final reviewQueue = AmitySocialClient.newPostRepository() .getPosts() .targetCommunity(communityId) .feedType(AmityFeedType.REVIEWING) .getLiveCollection(pageSize: 20); reviewQueue.getStreamController().stream.listen((posts) { final count = posts.length; }); await reviewQueue.loadNext(); ``` ## Notes - Show approve and decline actions only to users who should moderate the community. - Approval and decline calls operate by post ID and rely on backend permission checks. - Remove reviewed posts from local review queues after successful action, or wait for the live collection to update. ## Related Topics Query community feeds by review status. Remove posts during moderation. Display pinned posts. --- ### [Post Impressions](https://learn.social.plus/social-plus-sdk/social/content-management/posts/analytics/post-impressions) > Record post view analytics, read impression and reach counts, and query users who viewed a post. Post impression analytics has three SDK-facing pieces: - Mark a post as viewed when your app decides it was visible enough to count. - Read `impression` and `reach` from the post model returned by retrieval or query APIs. - Query reached users when you need the list of unique viewers for a post. The SDK exposes impression and reach counters, but it does not expose a ready-made view-rate metric. Calculate product-specific ratios in your app from the counters and audience size that matter to your experience. ## Metrics | Metric | TypeScript / iOS / Flutter | Android | Meaning | | --- | --- | --- | --- | | Impression | `post.impression` | `post.getImpression()` | Total view events recorded for the post | | Reach | `post.reach` | `post.getReach()` | Unique users who viewed the post | ## Platform APIs | Operation | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Mark viewed | `post.analytics.markAsViewed()` | `post.analytics.markAsViewed()` | `post.analytics().markAsViewed()` | `post.analytics().markPostAsViewed()` | | Query reached users | `UserRepository.getReachedUsers({ viewId, viewedType: "post" }, callback)` | `userRepository.getReachedUsers(viewedType: .post, viewedId: postId)` | `userRepository.getReachedUsers(AmityViewedType.POST, postId)` | `userRepository.getViewedUsers(viewedType: AmityViewedType.POST, viewedId: postId)` | ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Mark a post as viewed | Post object | Yes | Loaded post object whose analytics helper records the view. | | Query reached users | `viewId` / `viewedId` | Yes | Post ID used to query users reached by the post. | | Query reached users | `viewedType` | Yes | Viewed type value for posts. | | Query reached users | `limit` | No | TypeScript page size for reached-user results. | ## Mark a Post as Viewed Call the analytics method from your own visibility logic, such as a post detail screen opening or a feed cell crossing your viewability threshold. Do not call it from every render or rebuild. ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const unsubscribe = PostRepository.getPost( postId, ({ data: post, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } if (!post) return; post.analytics.markAsViewed(); renderResults({ impression: post.impression, reach: post.reach, }); }, ); ``` ```swift iOS let postRepository = AmityPostRepository() var token: AmityNotificationToken? token = postRepository.getPost(withId: "post-id").observe { liveObject, error in guard let post = liveObject.snapshot else { return } post.analytics.markAsViewed() let impression = post.impression let reach = post.reach showSuccessMessage("Impressions: \(impression), reach: \(reach)") } ``` ```kotlin Android postRepository.getPost(postId) .subscribe( { post -> post.analytics().markAsViewed() val impression = post.getImpression() val reach = post.getReach() showSuccessMessage("Impressions: $impression, reach: $reach") }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final subscription = AmitySocialClient.newPostRepository() .live .getPost(postId) .listen((AmityPost post) { post.analytics().markPostAsViewed(); final impression = post.impression ?? 0; final reach = post.reach ?? 0; }); await subscription.cancel(); ``` ## Query Reached Users Use reached-user queries when you need the user list behind the reach count. TypeScript names the ID parameter `viewId`; iOS, Android, and Flutter name it `viewedId`. ```typescript TypeScript import { UserRepository } from "@amityco/ts-sdk"; const unsubscribe = UserRepository.getReachedUsers( { viewId: postId, viewedType: "post", limit: 10, }, ({ data: users, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(users); }, ); ``` ```swift iOS let userRepository = AmityUserRepository() var token: AmityNotificationToken? token = userRepository .getReachedUsers(viewedType: .post, viewedId: "post-id") .observe { collection, error in showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.core.analytics.AmityViewedType AmityCoreClient.newUserRepository() .getReachedUsers(viewedType = AmityViewedType.POST, viewedId = postId) .subscribe( { users: PagingData -> showSuccessMessage(users) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final users = await AmityCoreClient.newUserRepository() .getViewedUsers(viewedType: AmityViewedType.POST, viewedId: postId) .query(); final reachedUserCount = users.length; ``` ## Notes - Keep the unsubscriber, notification token, or stream subscription so you can dispose it when the screen is destroyed. - Refresh or re-query the post if your UI needs updated `impression` and `reach` values after marking a view. - Treat analytics failures as non-blocking; viewing a post should not depend on the analytics event being accepted. - Apply your own viewability threshold and debounce rules before calling the SDK method. ## Related Topics Query posts by feed target, post type, review status, or tags Render post data and child media safely Track story views and query reached users for stories ## Social — Comments ### [Comments Overview](https://learn.social.plus/social-plus-sdk/social/content-management/comments/overview) > Create, query, update, delete, and react to comments on posts, stories, and custom content. Use the comment repository when you need discussions on posts, stories, or custom content. Comment creation supports text, optional image attachments, metadata, user mentions, and replies through `parentId`. A reply is a normal comment with `parentId` set to another comment ID. There is no separate reply API in the SDK. ## Comment Targets | Reference type | Use it for | SDK value | | --- | --- | --- | | Post | Comments on Social posts | `post` / `.post` / `POST` | | Story | Comments on story content | `story` / `.story` / `STORY` | | Content | Comments on your own external content IDs | `content` / `.content` / `CONTENT` | ## Comment Shape The exact property names vary by platform, but the SDK model centers on the same fields. | Concept | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Comment ID | `comment.commentId` | `comment.commentId` | `comment.getCommentId()` | `comment.commentId` | | Parent comment | `comment.parentId` | `comment.parentId` | `comment.getParentId()` | `comment.parentId` | | Reference | `referenceId`, `referenceType` | `referenceId`, `referenceType` | `comment.getReference()` | `referenceId`, `referenceType` | | Text data | `comment.data.text` | `comment.data` text value | `comment.getData()` | `comment.data` | | Image attachments | `comment.attachments` | `comment.attachments` | `comment.getAttachments()` | `comment.attachments` | | Reply count | `childrenNumber` | `childrenNumber` | `getChildCount()` | `childrenNumber` | | Reactions | `reactions`, `myReactions` | reaction properties on comment | `getReactionCount()`, `getMyReactions()` | `reactionCount`, `myReactions` | | Moderation | `isDeleted`, flags | `isDeleted`, flags | `isDeleted()`, flags | `isDeleted`, flags | ## Creation APIs | Platform | Repository entry point | Text comment | Image comment | | --- | --- | --- | --- | | TypeScript | `CommentRepository.createComment(...)` | `data: { text }` | `attachments: [{ type: "image", fileId }]` | | iOS | `AmityCommentRepository().createComment(with:)` | `AmityCommentCreateOptions(text:)` | `attachments: [.image(fileId:)]` | | Android | `commentRepository.createComment()` | `.with().text(...).build().send()` | `.with().attachments(...).build().send()` | | Flutter | `AmitySocialClient.newCommentRepository().createComment()` | `.create().text(...).send()` | `.create().attachments(...).send()` | Upload image files first, then pass the uploaded image file IDs to the comment creation API. Comment creation does not upload local files by itself. ## Related Topics Create top-level comments and replies with text Create comments with uploaded image attachments Query top-level comments and reply threads Edit, delete, flag, and manage comments --- ### [Create Comment](https://learn.social.plus/social-plus-sdk/social/content-management/comments/creation/create-comment) > Choose the right comment creation API for text comments, replies, and image comments. Use comment creation APIs to add comments to posts, stories, or your own custom content IDs. A reply is a normal comment with `parentId` set to the comment being replied to. Create top-level comments and replies with text, metadata, mentions, and supported link payloads. Create comments with uploaded image file IDs. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `referenceId` | Yes | Target content ID for the comment, such as a post ID. | | `referenceType` | Yes | Target content type, such as `post`. | | `text` | Yes | Text body for the comment or reply shown in these examples. | | `parentId` | No | Parent comment ID when creating a reply. | | `metadata` | No | App-defined metadata stored with the comment, where supported by the platform-specific builder. | | `mentionees` / mention builders | No | Mention targets for text comments, where supported by the platform-specific builder. | ## Basic Text Comment Create a top-level text comment by passing the reference target and text body to the comment repository. ```typescript TypeScript import { CommentRepository } from '@amityco/ts-sdk'; const { data: comment } = await CommentRepository.createComment({ referenceId: postId, referenceType: 'post', data: { text: 'Hello world!', }, }); renderResults(comment); ``` ```swift iOS let options = AmityCommentCreateOptions( referenceId: "post-id", referenceType: .post, text: "Hello world!" ) let comment = try await commentRepository.createComment(with: options) showSuccessMessage(comment.commentId) ``` ```kotlin Android AmitySocialClient.newCommentRepository() .createComment() .post(postId = postId) .with() .text(text = "Hello world!") .build() .send() .subscribe( { comment -> showSuccessMessage(comment.getCommentId()) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final comment = await AmitySocialClient.newCommentRepository() .createComment() .post(postId) .create() .text('Hello world!') .send(); final commentId = comment.commentId; ``` ## Reply to a Comment Create a reply with the same reference target and the `parentId` of the comment being replied to. ```typescript TypeScript import { CommentRepository } from '@amityco/ts-sdk'; const { data: reply } = await CommentRepository.createComment({ referenceId: postId, referenceType: 'post', parentId: commentId, data: { text: 'Replying to this comment', }, }); renderResults(reply); ``` ```swift iOS let options = AmityCommentCreateOptions( referenceId: "post-id", referenceType: .post, text: "Replying to this comment", parentId: "parent-comment-id" ) let reply = try await commentRepository.createComment(with: options) showSuccessMessage(reply.commentId) ``` ```kotlin Android AmitySocialClient.newCommentRepository() .createComment() .post(postId = postId) .parentId(parentId = commentId) .with() .text(text = "Replying to this comment") .build() .send() .subscribe( { reply -> showSuccessMessage(reply.getCommentId()) }, { error -> handleGeneralError(error) }, ) ``` ```dart Flutter final reply = await AmitySocialClient.newCommentRepository() .createComment() .post(postId) .parentId(commentId) .create() .text('Replying to this comment') .send(); final replyId = reply.commentId; ``` ## Related Topics See full text-comment parameters and notes. Create comments with uploaded image attachments. Query top-level comments and reply threads. Remove comments after user or moderation actions. --- ### [Text Comment](https://learn.social.plus/social-plus-sdk/social/content-management/comments/creation/text-comment) > Create text comments and replies on posts, stories, or custom content. Create a text comment by passing a reference target and text body to the comment repository. To create a reply, pass the parent comment ID with the same reference target. ## Parameters | Parameter | Required | Description | | --- | --- | --- | | `referenceId` | Yes | ID of the post, story, or custom content item being commented on | | `referenceType` | Yes | Use `post`, `story`, or `content` | | `text` | Yes | Text body of the comment | | `parentId` | No | Existing comment ID when creating a reply | | `metadata` | No | Custom metadata attached to the comment | | `mentionees` / mention builder | No | User mention payloads | | `links` | No | Link metadata on TypeScript, iOS, and Android | The current Flutter public comment creation builder exposes `metadata(...)` and `mentionUsers(...)`, but it does not expose a `links(...)` method. ## Create a Text Comment Create a top-level text comment with the reference target and text body. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const { data: comment } = await CommentRepository.createComment({ referenceId: postId, referenceType: "post", data: { text: "Hello world!", }, }); renderResults(comment); ``` ```swift iOS let options = AmityCommentCreateOptions( referenceId: "post-id", referenceType: .post, text: "Hello world!" ) let comment = try await commentRepository.createComment(with: options) showSuccessMessage(comment.commentId) ``` ```kotlin Android AmitySocialClient.newCommentRepository() .createComment() .post(postId = postId) .with() .text(text = "Hello world!") .build() .send() .subscribe( { comment -> showSuccessMessage(comment.getCommentId()) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final comment = await AmitySocialClient.newCommentRepository() .createComment() .post(postId) .create() .text('Hello world!') .send(); final commentId = comment.commentId; ``` ## Reply to a Comment Replies use the same creation API with `parentId` set to the comment being replied to. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const { data: reply } = await CommentRepository.createComment({ referenceId: postId, referenceType: "post", parentId: commentId, data: { text: "Replying to this comment", }, }); renderResults(reply); ``` ```swift iOS let options = AmityCommentCreateOptions( referenceId: "post-id", referenceType: .post, text: "Replying to this comment", parentId: "parent-comment-id" ) let reply = try await commentRepository.createComment(with: options) showSuccessMessage(reply.commentId) ``` ```kotlin Android AmitySocialClient.newCommentRepository() .createComment() .post(postId = postId) .parentId(parentId = commentId) .with() .text(text = "Replying to this comment") .build() .send() .subscribe( { reply -> showSuccessMessage(reply.getCommentId()) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final reply = await AmitySocialClient.newCommentRepository() .createComment() .post(postId) .parentId(commentId) .create() .text('Replying to this comment') .send(); final replyId = reply.commentId; ``` ## Notes - Keep the same `referenceId` and `referenceType` as the thread you are replying in. - Use query APIs or live collections to display creation and sync state in the UI. - Validate your product's text limits and moderation rules before calling the SDK. - Build mention payloads from the user IDs your mention picker returns. ## Related Topics Create comments with uploaded image attachments. Query top-level comments and reply threads. --- ### [Image Comment](https://learn.social.plus/social-plus-sdk/social/content-management/comments/creation/image-comment) > Create comments with uploaded image attachments. Image comments are comments with image attachment file IDs. Upload each image first, then pass the uploaded file ID to the comment creation API. You can also include text with the image attachment. Comment creation accepts uploaded image file IDs. Use the file or image upload APIs before creating the comment. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Create an image comment | `referenceId` | Yes | Target content ID for the comment, such as a post ID. | | Create an image comment | `referenceType` | Yes | Target content type, such as `post`. | | Create an image comment | Uploaded image file ID | Yes | File ID returned by the image upload API. | | Create an image comment | `text` | No | Optional text body included with the image attachment. | | Reply with an image | `parentId` | Yes | Parent comment ID when creating an image reply. | ## Create an Image Comment Create an image comment from an uploaded image file ID, with optional text where supported. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const { data: comment } = await CommentRepository.createComment({ referenceId: postId, referenceType: "post", data: { text: "Photo response", }, attachments: [ { type: "image", fileId: imageFileId, }, ], }); renderResults(comment); ``` ```swift iOS let options = AmityCommentCreateOptions( referenceId: "post-id", referenceType: .post, text: "Photo response", attachments: [ .image(fileId: "uploaded-image-id") ] ) let comment = try await commentRepository.createComment(with: options) showSuccessMessage(comment.commentId) ``` ```kotlin Android val imageAttachment = AmityComment.Attachment.IMAGE( fileId = fileId, image = null ) AmitySocialClient.newCommentRepository() .createComment() .post(postId = postId) .with() .attachments(imageAttachment) .text(text = "Photo response") .build() .send() .subscribe( { comment -> showSuccessMessage(comment.getCommentId()) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final comment = await AmitySocialClient.newCommentRepository() .createComment() .post(postId) .create() .attachments([ CommentImageAttachment(fileId: fileId), ]) .text('Photo response') .send(); final commentId = comment.commentId; ``` ## Reply with an Image Set `parentId` before the platform-specific create step. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const { data: reply } = await CommentRepository.createComment({ referenceId: postId, referenceType: "post", parentId: commentId, attachments: [ { type: "image", fileId: imageFileId, }, ], }); renderResults(reply); ``` ```swift iOS let options = AmityCommentCreateOptions( referenceId: "post-id", referenceType: .post, text: "", attachments: [ .image(fileId: "uploaded-image-id") ], parentId: "parent-comment-id" ) let reply = try await commentRepository.createComment(with: options) showSuccessMessage(reply.commentId) ``` ```kotlin Android val imageAttachment = AmityComment.Attachment.IMAGE( fileId = fileId, image = null ) AmitySocialClient.newCommentRepository() .createComment() .post(postId = postId) .parentId(parentId = commentId) .with() .attachments(imageAttachment) .build() .send() .subscribe( { reply -> showSuccessMessage(reply.getCommentId()) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final reply = await AmitySocialClient.newCommentRepository() .createComment() .post(postId) .parentId(commentId) .create() .attachments([ CommentImageAttachment(fileId: fileId), ]) .send(); final replyId = reply.commentId; ``` ## Notes - Reuse the uploaded image file ID returned by the upload API; do not pass a local file path to comment creation. - If your product needs image moderation or file-size limits, apply those rules before upload. - Query the resulting comment if you need composed file metadata such as rendered image information. ## Related Topics Create text comments and replies. Upload image files before attaching them to comments. --- ### [Get Comment](https://learn.social.plus/social-plus-sdk/social/content-management/comments/retrieval/get-comment) > Retrieve a comment by ID and observe updates where the SDK exposes live objects or streams. Use a comment ID when your app needs to open a thread, refresh a detail view, or inspect a comment after creation. TypeScript, iOS, Android, and Flutter all expose single-comment retrieval; the live update shape differs by platform. ## Comment Fields The comment model names vary by SDK, but these are the fields most apps read after retrieval. | Concept | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Comment ID | `commentId` | `commentId` | `getCommentId()` | `commentId` | | Parent comment | `parentId` | `parentId` | `getParentId()` | `parentId` | | Reference | `referenceId`, `referenceType` | `referenceId`, `referenceType` | `getReference()` | `referenceId`, `referenceType` | | Data | `data` | `data` | `getData()` | `data` | | Image attachments | `attachments` | `attachments` | `getAttachments()` | `attachments` | | Reply count | `childrenNumber` | `childrenNumber` | `getChildCount()` | `childrenNumber` | | Reactions | `reactionCount`, `myReactions` | `reactionsCount`, `myReactions` | `getReactionCount()`, `getMyReactions()` | `reactionCount`, `myReactions` | | Deleted state | `isDeleted` | `isDeleted` | `isDeleted()` | `isDeleted` | ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Observe a comment | `commentId` | Yes | Comment ID to observe as a live object or stream. | | Fetch one comment | `commentId` | Yes | Comment ID to fetch once on Flutter. | | Fetch multiple comments by ID | `commentIds` | Yes | Comment IDs to fetch on TypeScript and Android. | ## Observe a Comment Observe a single comment when a detail view or thread preview should stay current after edits, deletes, or reactions. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const unsubscribe = CommentRepository.getComment( commentId, ({ data: comment, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } if (comment) { renderResults(comment); } }, ); ``` ```swift iOS var token: AmityNotificationToken? token = commentRepository .getComment(withId: "comment-id") .observe { liveObject, error in if let error { handleError(error) return } guard let comment = liveObject.snapshot else { return } showSuccessMessage(comment.commentId) } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.core.ExperimentalAmityApi @OptIn(ExperimentalAmityApi::class) fun observeComment(commentId: String) { AmitySocialClient.newCommentRepository() .getComment(commentId = commentId) .subscribe( { comment -> showSuccessMessage(comment.getCommentId()) }, { error -> handleGeneralError(error) } ) } observeComment(commentId) ``` ```dart Flutter final commentStream = AmitySocialClient.newCommentRepository() .live .getComment(commentId); final subscription = commentStream.listen((comment) { final latestCommentId = comment.commentId; }, onError: (error) { showError(error); }); await subscription.cancel(); ``` ## Fetch One Comment Flutter also exposes a direct one-shot fetch for comment detail screens. ```dart Flutter final comment = await AmitySocialClient.newCommentRepository() .getComment(commentId: commentId); final fetchedCommentId = comment.commentId; ``` ## Fetch Multiple Comments by ID Batch lookup is available in TypeScript and Android. Use query APIs when you need a paged collection for a post, story, or custom content item. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const { data: comments } = await CommentRepository.getCommentByIds([ commentId, "another-comment-id", ]); renderResults(comments); ``` ```kotlin Android AmitySocialClient.newCommentRepository() .getCommentByIds(commentIds = setOf(commentId, "another-comment-id")) .subscribe( { comments -> showSuccessMessage(comments.size) }, { error -> handleGeneralError(error) } ) ``` ## Notes - Dispose of live observers or stream subscriptions when the screen is no longer active. - Android `getComment(...)` is annotated with `ExperimentalAmityApi`; opt in at the call site or enclosing scope. - If the user needs a list of comments under a reference, use [Query Comments](/social-plus-sdk/social/content-management/comments/retrieval/query-comments) instead of repeated single-comment calls. ## Related Topics Query top-level comments and reply threads. Fetch or derive the newest comment for a reference. --- ### [Get Latest Comment](https://learn.social.plus/social-plus-sdk/social/content-management/comments/retrieval/get-latest-comment) > Fetch the newest comment for a post or content item, with platform-specific helper availability. Use the latest-comment pattern when you need a compact preview, such as showing the newest comment below a post card. iOS and Android expose dedicated latest-comment helpers. TypeScript and Flutter use the normal comment query API sorted newest-first with a page size of one. ## Platform Availability | Platform | Dedicated latest helper | Reference targets | | --- | --- | --- | | iOS | `getLatestComment(withReferenceId:referenceType:includeReplies:)` | `post`, `content`, `story` | | Android | `getLatestComment().post(...)` / `.content(...)` | `post`, `content` | | TypeScript | Use `getComments(...)` with `sortBy: "lastCreated"` and `pageSize: 1` | `post`, `content`, `story` | | Flutter | Use `getComments().post(...)`, `.content(...)`, or `.story(...)` with newest-first sort and limit `1` | `post`, `content`, `story` | ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Dedicated helpers | `referenceId` | Yes | Target content ID whose latest comment should be fetched. | | Dedicated helpers | `referenceType` | Yes | Target content type, such as `post`. | | Dedicated helpers | `includeReplies` | No | Whether replies can be returned as the latest comment on iOS and Android. | | Query the latest comment | `sortBy` | Yes | Newest-first sort order, such as `lastCreated`. | | Query the latest comment | `pageSize` / `limit` | Yes | Set to `1` to retrieve only the newest comment. | ## Dedicated Helpers Use the dedicated iOS and Android helpers when you want the latest comment without building a manual one-item query. ```swift iOS let latestComment = try await commentRepository.getLatestComment( withReferenceId: "post-id", referenceType: .post, includeReplies: true ) showSuccessMessage(latestComment.commentId) ``` ```kotlin Android AmitySocialClient.newCommentRepository() .getLatestComment() .post(postId = postId) .includeReplies(includeReplies = true) .build() .query() .subscribe( { comment -> showSuccessMessage(comment.getCommentId()) }, { error -> handleGeneralError(error) } ) ``` ## Query the Latest Comment Use this pattern on TypeScript and Flutter, or when you want the same query-based behavior across platforms. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const unsubscribe = CommentRepository.getComments( { referenceType: "post", referenceId: postId, sortBy: "lastCreated", pageSize: 1, }, ({ data: comments, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } const latestComment = comments?.[0]; if (latestComment) { renderResults(latestComment); } }, ); ``` ```dart Flutter final comments = await AmitySocialClient.newCommentRepository() .getComments() .post(postId) .sortBy(AmityCommentSortOption.LAST_CREATED) .includeDeleted(false) .query(limit: 1); final latestComment = comments.isNotEmpty ? comments.first : null; ``` ## Include Replies For iOS and Android dedicated helpers, `includeReplies` controls whether replies can be returned as the latest comment. | Value | Result | | --- | --- | | `true` | The newest comment at any level can be returned | | `false` | Only top-level comments are considered | For query-based implementations, use the `parentId` filter: - Omit `parentId` to query comments from all levels where the SDK supports it. - Set `parentId` to `null` / `nil` to query only top-level comments. - Set `parentId` to a comment ID to query replies to that comment. ## Notes - The dedicated iOS and Android latest-comment helpers return one comment, not a live object or live collection. - Use [Query Comments](/social-plus-sdk/social/content-management/comments/retrieval/query-comments) when you need pagination, filtering, or a full thread. - Handle the empty state in query-based implementations because a reference may not have comments yet. ## Related Topics Query comments with parent filters, deleted-state filters, and pagination. Retrieve a specific comment by ID. --- ### [Query Comments](https://learn.social.plus/social-plus-sdk/social/content-management/comments/retrieval/query-comments) > Query comments for posts, stories, or custom content with pagination, parent filtering, deleted-state filtering, and sort order. Use comment queries when your app needs a paged list for a post, story, or custom content item. Query top-level comments for the main thread, then query replies with `parentId` when a user expands a comment. Query APIs return paged/live collection results on TypeScript, iOS, Android, and Flutter. For a single known comment ID, use [Get Comment](/social-plus-sdk/social/content-management/comments/retrieval/get-comment). ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Query comments | `referenceId` | Yes | ID of the post, story, or custom content item. | | Query comments | `referenceType` | Yes | Target content type, such as `post`, `story`, or `content`. | | Query comments | `parentId` | No | `null` / `nil` for top-level comments, a comment ID for replies, or omitted where supported. | | Query comments | `includeDeleted` | No | Include soft-deleted comments for moderation or audit views. | | Query comments | `dataTypes` | No | Filter comments by content type, such as text or image. | | Query comments | `sortBy` / `orderBy` | No | Newest-first or oldest-first comment order. | | Query comments | `pageSize` / `limit` | No | Number of comments to load per page. | ## Query Options | Option | Description | | --- | --- | | `referenceId` | ID of the post, story, or custom content item | | `referenceType` | `post`, `story`, or `content` | | `parentId` | `null` / `nil` for top-level comments; a comment ID for replies; omit the parent filter to include all levels where supported | | `includeDeleted` | Include soft-deleted comments when building moderation or audit views | | `dataTypes` | Filter comments by content type, such as text or image | | `sortBy` / `orderBy` | Newest-first or oldest-first comment order | | `pageSize` / `limit` | Number of comments to load per page | ## Query Top-Level Comments Set the parent filter to `null` / `nil` when you only want root comments. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const unsubscribe = CommentRepository.getComments( { referenceType: "post", referenceId: postId, parentId: null, sortBy: "lastCreated", pageSize: 10, }, ({ data: comments, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(comments); }, ); ``` ```swift iOS let queryOptions = AmityCommentQueryOptions( referenceId: "post-id", referenceType: .post, filterByParentId: true, parentId: nil, orderBy: .descending, includeDeleted: false, pageSize: 20 ) token = commentRepository .getComments(with: queryOptions) .observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.social.comment.query.AmityCommentSortOption AmitySocialClient.newCommentRepository() .getComments() .post(postId = postId) .parentId(parentId = null) .sortBy(AmityCommentSortOption.LAST_CREATED) .includeDeleted(includeDeleted = false) .pageSize(pageSize = 15) .build() .query() .subscribe( { pagingData -> getPagingData(pagingData) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final liveCollection = AmitySocialClient.newCommentRepository() .getComments() .post(postId) .parentId(null) .sortBy(AmityCommentSortOption.LAST_CREATED) .includeDeleted(false) .getLiveCollection(pageSize: 20); final subscription = liveCollection.getStreamController().stream.listen( (comments) { final visibleCommentCount = comments.length; }, onError: (error) { showError(error); }, ); liveCollection.loadNext(); await subscription.cancel(); ``` ## Query Replies Set `parentId` to an existing comment ID to load replies for that comment. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const unsubscribe = CommentRepository.getComments( { referenceType: "post", referenceId: postId, parentId: commentId, sortBy: "firstCreated", pageSize: 5, }, ({ data: replies, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(replies); }, ); ``` ```swift iOS let replyQueryOptions = AmityCommentQueryOptions( referenceId: "post-id", referenceType: .post, filterByParentId: true, parentId: "parent-comment-id", orderBy: .ascending, includeDeleted: false, pageSize: 5 ) token = commentRepository .getComments(with: replyQueryOptions) .observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.social.comment.query.AmityCommentSortOption AmitySocialClient.newCommentRepository() .getComments() .post(postId = postId) .parentId(parentId = commentId) .sortBy(AmityCommentSortOption.FIRST_CREATED) .includeDeleted(includeDeleted = false) .pageSize(pageSize = 5) .build() .query() .subscribe( { pagingData -> getPagingData(pagingData) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final replies = await AmitySocialClient.newCommentRepository() .getComments() .post(postId) .parentId(commentId) .sortBy(AmityCommentSortOption.FIRST_CREATED) .includeDeleted(false) .query(limit: 5); final replyCount = replies.length; ``` ## Filter by Data Type Use data type filters when your UI needs a specific type of comment, such as image-only comments. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const unsubscribe = CommentRepository.getComments( { referenceType: "post", referenceId: postId, dataTypes: { values: ["image"], matchType: "exact", }, pageSize: 10, }, ({ data: imageComments, error }) => { if (error) { handleError(error); return; } renderResults(imageComments); }, ); ``` ```swift iOS let imageOnlyOptions = AmityCommentQueryOptions( referenceId: "post-id", referenceType: .post, filterByParentId: false, dataTypes: .exact([.image]), orderBy: .descending, includeDeleted: false, pageSize: 20 ) token = commentRepository .getComments(with: imageOnlyOptions) .observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.social.comment.query.AmityCommentDataTypeFilter import com.amity.socialcloud.sdk.api.social.comment.query.AmityCommentSortOption AmitySocialClient.newCommentRepository() .getComments() .post(postId = postId) .dataTypes( AmityCommentDataTypeFilter.Exact( dataTypes = listOf(AmityComment.DataType.IMAGE) ) ) .sortBy(AmityCommentSortOption.LAST_CREATED) .includeDeleted(includeDeleted = false) .pageSize(pageSize = 10) .build() .query() .subscribe( { pagingData -> getPagingData(pagingData) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final imageComments = await AmitySocialClient.newCommentRepository() .getComments() .post(postId) .dataTypes( AmityCommentDataTypeFilter.exact(dataTypes: [AmityDataType.IMAGE]), ) .sortBy(AmityCommentSortOption.LAST_CREATED) .includeDeleted(false) .query(limit: 10); final imageCommentCount = imageComments.length; ``` ## Reference Targets | Target | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Post | `referenceType: "post"` | `.post` | `.post(postId = ...)` | `.post(postId)` | | Story | `referenceType: "story"` | `.story` | `.story(storyId = ...)` | `.story(storyId)` | | Content | `referenceType: "content"` | `.content` | `.content(contentId = ...)` | `.content(contentId)` | ## Notes - Keep `referenceId` and `referenceType` consistent between the parent comment and its replies. - Use smaller page sizes for reply threads than for top-level comments. - `includeDeleted: true` is mainly for moderation, audit, or admin-style views; most end-user views should exclude deleted comments. - Dispose of observers, subscriptions, or live collections when the screen is no longer active. ## Related Topics Retrieve a specific comment by ID Create top-level comments and replies --- ### [Edit Comment](https://learn.social.plus/social-plus-sdk/social/content-management/comments/actions/edit-comment) > Update comment text, metadata, mentions, links, and image attachments with the comment update APIs. Use comment update APIs when a user edits a comment or replaces its image attachments. The SDK sends the update to the server; permission, ownership, and moderation rules are enforced by the backend and surfaced as SDK errors. ## Update Fields | Field | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Text | `data: { text }` | `AmityCommentUpdateOptions(text:)` | `.text(...)` | `.text(...)` | | Metadata | `metadata` | `metadata` | `.metadata(...)` | `.metadata(...)` | | Mentions | `mentionees` | `mentioneesBuilder` | `.mentionUsers(...)` | `.mentionUsers(...)` | | Links | `links` | `links` | `.links(...)` | Not exposed on the public update builder | | Image attachments | `attachments` payload | `attachments` | `.attachments(...)` | `.attachments(...)` | Upload image files first, then pass the uploaded file IDs to the update API. Passing local file paths to comment update is not supported. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Update text | `commentId` | Yes | Comment ID to update. | | Update text | `text` | Yes | Replacement text body for the comment. | | Update image attachments | `commentId` | Yes | Comment ID to update. | | Update image attachments | Uploaded image file ID | Yes | Uploaded image file ID to keep on the comment. | | Update optional fields | `metadata`, mentions, links | No | Optional editable fields where the target SDK exposes them. | ## Update Text Update text and other editable fields by loading the comment ID and applying the platform-specific update call. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const { data: updatedComment } = await CommentRepository.updateComment( commentId, { data: { text: "Updated comment text", }, }, ); renderResults(updatedComment); ``` ```swift iOS let options = AmityCommentUpdateOptions( text: "Updated comment text" ) let updatedComment = try await commentRepository.editComment( withId: "comment-id", options: options ) showSuccessMessage(updatedComment.commentId) ``` ```kotlin Android AmitySocialClient.newCommentRepository() .editComment(commentId = commentId) .text(text = "Updated comment text") .build() .apply() .subscribe( { updatedComment -> showSuccessMessage(updatedComment.getCommentId()) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter await AmitySocialClient.newCommentRepository() .updateComment(commentId: commentId) .text('Updated comment text') .build() .update(); ``` ## Update Image Attachments Set the attachment list to the full list you want the comment to keep. To preserve an existing image, include its uploaded file ID in the update payload. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const patch: Parameters[0] = { referenceId: postId, referenceType: "post", data: { text: "Updated with an image", }, attachments: [ { type: "image", fileId: imageFileId, }, ], }; const { data: updatedComment } = await CommentRepository.updateComment( commentId, patch, ); renderResults(updatedComment); ``` ```swift iOS let options = AmityCommentUpdateOptions( text: "Updated with an image", attachments: [ .image(fileId: "uploaded-image-id") ] ) let updatedComment = try await commentRepository.editComment( withId: "comment-id", options: options ) showSuccessMessage(updatedComment.commentId) ``` ```kotlin Android val imageAttachment = AmityComment.Attachment.IMAGE( fileId = fileId, image = null ) AmitySocialClient.newCommentRepository() .editComment(commentId = commentId) .text(text = "Updated with an image") .attachments(imageAttachment) .build() .apply() .subscribe( { updatedComment -> showSuccessMessage(updatedComment.getCommentId()) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter await AmitySocialClient.newCommentRepository() .updateComment(commentId: commentId) .text('Updated with an image') .attachments([ CommentImageAttachment(fileId: fileId), ]) .build() .update(); ``` ## Clear Optional Fields Different SDKs distinguish between "leave this field unchanged" and "replace this field with an empty value." | Change | Pattern | | --- | --- | | Leave text unchanged | Do not set the text field in the update options | | Remove text from a media comment | Set text to an empty string | | Leave attachments unchanged | Do not set the attachments field | | Remove all attachments | Set attachments to an empty list where the platform exposes that update shape | | Remove metadata | Set metadata to an empty object/dictionary | | Remove links | Set links to an empty list where the platform exposes link updates | ## Notes - For TypeScript image attachment updates, build the attachment payload using the same uploaded-file shape as comment creation. - For iOS, Android, and Flutter, `nil` / unset attachment fields leave existing attachments unchanged; an explicit empty list removes attachments. - Rebuild mention and link payloads from the edited text before updating a comment. - Handle permission, not-found, and moderation errors from the SDK call rather than relying only on client-side checks. ## Related Topics Soft delete or permanently delete comments where supported. Query comments and include deleted comments when needed. Upload images before attaching them to comments. Create top-level comments and replies. --- ### [Delete Comment](https://learn.social.plus/social-plus-sdk/social/content-management/comments/actions/delete-comment) > Delete comments with the platform-supported soft delete and hard delete APIs. Use comment deletion when a user removes their own comment or a moderation flow removes a comment. Soft delete marks the comment as deleted while preserving enough state for collections and audit-style views. Hard delete permanently removes the comment where the SDK exposes that operation. ## Platform Support | Platform | Soft delete | Hard delete | | --- | --- | --- | | TypeScript | `CommentRepository.softDeleteComment(commentId)` or `deleteComment(commentId)` | `CommentRepository.hardDeleteComment(commentId)` or `deleteComment(commentId, true)` | | iOS | `softDeleteComment(withId:)` | `hardDeleteComment(withId:)` | | Android | `softDeleteComment(commentId)` | `hardDeleteComment(commentId)` | | Flutter | `comment.delete()` | Not exposed as a verified separate public delete path | Hard delete is permanent. Confirm the action in your UI before calling a hard-delete API. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Soft delete | `commentId` | Yes | Comment ID to soft delete. | | Hard delete | `commentId` | Yes | Comment ID to hard delete where supported. | | Query deleted comments | `referenceId` | Yes | Target content ID whose comments should be queried. | | Query deleted comments | `referenceType` | Yes | Target content type, such as `post`. | | Query deleted comments | `includeDeleted` | Yes | Include soft-deleted comments in query results. | ## Soft Delete Soft delete a comment when the UI should preserve deleted-state semantics in comment collections. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const deletedComment = await CommentRepository.softDeleteComment(commentId); renderResults(deletedComment); ``` ```swift iOS try await commentRepository.softDeleteComment(withId: "comment-id") showSuccessMessage("Comment deleted") ``` ```kotlin Android AmitySocialClient.newCommentRepository() .softDeleteComment(commentId = commentId) .subscribe( { showSuccessMessage(commentId) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final comment = await AmitySocialClient.newCommentRepository() .getComment(commentId: commentId); await comment.delete(); ``` ## Hard Delete Hard delete is available in TypeScript, iOS, and Android. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const deletedComment = await CommentRepository.hardDeleteComment(commentId); renderResults(deletedComment); ``` ```swift iOS try await commentRepository.hardDeleteComment(withId: "comment-id") showSuccessMessage("Comment permanently deleted") ``` ```kotlin Android AmitySocialClient.newCommentRepository() .hardDeleteComment(commentId = commentId) .subscribe( { showSuccessMessage(commentId) }, { error -> handleGeneralError(error) } ) ``` ## Query Deleted Comments Use the query API with deleted comments included when you need moderation or audit views. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const unsubscribe = CommentRepository.getComments( { referenceType: "post", referenceId: postId, includeDeleted: true, pageSize: 10, }, ({ data: comments, error }) => { if (error) { handleError(error); return; } renderResults(comments); }, ); ``` ```swift iOS let options = AmityCommentQueryOptions( referenceId: "post-id", referenceType: .post, filterByParentId: false, orderBy: .descending, includeDeleted: true, pageSize: 20 ) token = commentRepository .getComments(with: options) .observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android AmitySocialClient.newCommentRepository() .getComments() .post(postId = postId) .includeDeleted(includeDeleted = true) .build() .query() .subscribe( { pagingData -> getPagingData(pagingData) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final comments = await AmitySocialClient.newCommentRepository() .getComments() .post(postId) .includeDeleted(true) .query(limit: 10); final resultCount = comments.length; ``` ## Notes - Handle not-found, permission, and moderation errors from the SDK call. - Query live collections after deletion if the UI needs to reflect deleted-state changes. - Use soft delete for reversible moderation workflows; reserve hard delete for cases where permanent removal is intended and supported on that platform. - The Flutter public comment extension exposes `delete()` for the comment instance; do not rely on a separate Flutter hard-delete path unless your SDK version documents one. ## Related Topics Update text, metadata, mentions, links, and image attachments. Query comments with deleted-state filtering. Retrieve a specific comment by ID. Understand comment targets and model fields. ## Social — Stories ### [Stories Overview](https://learn.social.plus/social-plus-sdk/social/content-management/stories/overview) > Create, retrieve, analyze, and delete time-limited image and video stories with the social.plus SDKs. Stories are time-limited media items attached to a story target. The SDKs support image and video stories, optional metadata, optional hyperlink story items, live retrieval, view/link analytics, and soft or hard deletion. The examples in this section use community targets, which are the common target shape across the SDKs covered here. For a product-level walkthrough, see [Stories & Ephemeral Content](/use-cases/social/stories-and-ephemeral-content). The pages in this SDK section stay focused on exact SDK methods and data shapes. Publish image or video stories with optional metadata and hyperlink items. Observe individual stories, active story collections, story targets, and global story targets. Mark stories as seen, mark hyperlink clicks, and query reached users where supported. ## Story Model The exact accessor style differs by platform, but story objects expose the same main concepts: | Field | Description | |-------|-------------| | `storyId` | Unique story identifier. | | `targetType` / `targetId` | Target that owns the story. The SDK examples use `community`. | | `dataType` | Story content type, such as image or video. | | `data` | Media-specific story data, including image/video file references where available. | | `items` / `storyItems` | Interactive story items such as hyperlinks. | | `metadata` | Custom metadata provided during creation. | | `syncState` / `state` | Local creation state such as syncing, synced, or failed. | | `isDeleted` | Whether the story is deleted. | | `isSeen` | Whether the current user has seen the story. | | `impression` | Total view count exposed on the story model. | | `reach` | Unique reached-user count exposed on the story model. | | `expiresAt`, `createdAt`, `updatedAt` | Story lifecycle timestamps. | ## Story Target Model Story targets represent story availability for a target such as a community. | Field | Description | |-------|-------------| | `targetType` / `targetId` | Target identity. | | `hasUnseen` | Whether the current user has unseen stories for this target. | | `syncingStoriesCount` | Local stories still syncing for this target. | | `failedStoriesCount` | Local stories that failed to sync for this target. | | `updatedAt` | Last target update timestamp. | ## SDK Surface | Workflow | TypeScript | iOS | Android | Flutter | |----------|------------|-----|---------|---------| | Create image | `createImageStory()` | `createImageStory(options:)` | `createImageStory()` | `createImageStory()` | | Create video | `createVideoStory()` | `createVideoStory(options:)` | `createVideoStory()` | `createVideoStory()` | | Single story | `getStoryByStoryId()` | `getStory(storyId:)` | `getStory()` | `live.getStory()` | | Active stories | `getActiveStoriesByTarget()` | `getActiveStoriesByTarget(...)` | `getActiveStories(...)` | `getActiveStories(...).build()` | | Stories by targets | `getStoriesByTargetIds()` | `getStoriesByTargets(...)` | `getStoriesByTargets(...)` | `getStoriesByTargets(...).build()` | | Story target | `getTargetById()` | `getStoryTarget(...)` | `getStoryTarget(...)` | `live.getStoryTaregt(...)` | | Story targets | `getTargetsByTargetIds()` | `getStoryTargets(...)` | `getStoryTargets(...)` | `getStoryTargets(...).build()` | | Global targets | `getGlobalStoryTargets()` | `getGlobalStoryTargets(option:)` | `getGlobalStoryTargets(...)` | `GlobalStoryTargetLiveCollection` | | Analytics | `story.analytics.*` | `story.analytics.*` | `storyRepository.analytics(story).*` | `storyRepository.analytics(story).*` | Flutter's single story-target helper is currently named `getStoryTaregt` in the shipped SDK. The spelling above is intentional. ## Accuracy Notes - The SDK creation APIs accept image or video files and pass upload/processing errors through their platform-specific async pattern. - File size, duration, and moderation limits are enforced by backend or network configuration. This SDK reference does not hardcode those values. - Story analytics methods should be called on synced story objects. Native and Flutter SDKs explicitly no-op for unsynced stories. - `SMART`, `UNSEEN`, `SEEN`, and `ALL` query options apply to global story target retrieval. ## Related Topics Publish image and video stories. Retrieve story live objects and collections. Retrieve target-level story availability and unseen status. Record story views, link clicks, and reached users. --- ### [Create Story](https://learn.social.plus/social-plus-sdk/social/content-management/stories/creation/create-story) > Create image and video stories with StoryRepository creation APIs. Use the story repository to create image or video stories for a target. The examples below use community targets, optional metadata, and an optional hyperlink story item. Story creation is optimistic on the SDKs that maintain local story state. Watch the returned story or active story collection for sync state changes such as syncing, synced, or failed. ## Parameters | Concept | TypeScript | iOS | Android | Flutter | |---------|------------|-----|---------|---------| | Target type | `'community'` | `.community` | `AmityStory.TargetType.COMMUNITY` | `AmityStoryTargetType.COMMUNITY` | | Target ID | `string` | `String` | `String` | `String` | | Image input | `FormData` with `files` | `imageFileURL` | `imageUri` | `imageFile` | | Video input | `FormData` with `files` | `videoFileURL` | `videoUri` | `videoFile` | | Metadata | Object | `[String: Any]?` | `JsonObject?` | `Map?` | | Hyperlinks | `Amity.StoryItemType.Hyperlink` | `AmityHyperLinkItem` | `AmityStoryItem.HYPERLINK` | `HyperLink` | For TypeScript, append the file under the `files` key. The SDK throws if the `FormData` object has no `files` entry. ## Create image story Create an image story from a local image file, target, and optional metadata or hyperlink items. ```typescript TypeScript import { StoryRepository } from '@amityco/ts-sdk'; const formData = new FormData(); formData.append('files', imageFile); const result = await StoryRepository.createImageStory( 'community', communityId, formData, { campaignId: 'launch' }, 'fit', [ { type: Amity.StoryItemType.Hyperlink, data: { url: 'https://example.com', customText: 'Learn more', }, }, ], ); renderResults(result.data); ``` ```swift iOS let items: [AmityStoryItem] = [ AmityHyperLinkItem(url: "https://example.com", customText: "Learn more") ] let imageURL = URL(fileURLWithPath: "/path/to/image.jpg") let options = AmityImageStoryCreateOptions( targetType: .community, targetId: communityId, imageFileURL: imageURL, metadata: ["campaignId": "launch"], items: items, imageDisplayMode: .fit ) let story = try await storyRepository.createImageStory(options: options) showSuccessMessage(story.storyId) ``` ```kotlin Android fun createImageStory( storyRepository: AmityStoryRepository, targetId: String, imageUri: Uri ) { storyRepository.createImageStory( targetType = AmityStory.TargetType.COMMUNITY, targetId = targetId, imageUri = imageUri, storyItems = listOf( AmityStoryItem.HYPERLINK( url = "https://example.com", customText = "Learn more" ) ), imageDisplayMode = AmityStoryImageDisplayMode.FIT, metadata = JsonObject().apply { addProperty("campaignId", "launch") } ) .doOnComplete { showSuccessMessage("Story created") } .doOnError { error -> showErrorMessage(error = error) } .subscribe() } ``` ```dart Flutter import 'dart:io'; Future createImageStory(String communityId) async { final storyRepository = AmitySocialClient.newStoryRepository(); await storyRepository.createImageStory( targetType: AmityStoryTargetType.COMMUNITY, targetId: communityId, imageFile: File('/path/to/image.jpg'), imageDisplayMode: AmityStoryImageDisplayMode.FIT, metadata: {'campaignId': 'launch'}, storyItems: [ HyperLink( url: 'https://example.com', customText: 'Learn more', ), ], ); } ``` ## Create video story Create a video story from a local video file, target, and optional metadata or hyperlink items. ```typescript TypeScript import { StoryRepository } from '@amityco/ts-sdk'; const formData = new FormData(); formData.append('files', videoFile); const result = await StoryRepository.createVideoStory( 'community', communityId, formData, { campaignId: 'launch' }, [ { type: Amity.StoryItemType.Hyperlink, data: { url: 'https://example.com', customText: 'Watch more', }, }, ], ); renderResults(result.data); ``` ```swift iOS let items: [AmityStoryItem] = [ AmityHyperLinkItem(url: "https://example.com", customText: "Watch more") ] let videoURL = URL(fileURLWithPath: "/path/to/video.mp4") let options = AmityVideoStoryCreateOptions( targetType: .community, targetId: communityId, videoFileURL: videoURL, metadata: ["campaignId": "launch"], items: items ) let story = try await storyRepository.createVideoStory(options: options) showSuccessMessage(story.storyId) ``` ```kotlin Android fun createVideoStory( storyRepository: AmityStoryRepository, targetId: String, videoUri: Uri ) { storyRepository.createVideoStory( targetType = AmityStory.TargetType.COMMUNITY, targetId = targetId, videoUri = videoUri, storyItems = listOf( AmityStoryItem.HYPERLINK( url = "https://example.com", customText = "Watch more" ) ), metadata = JsonObject().apply { addProperty("campaignId", "launch") } ) .doOnComplete { showSuccessMessage("Story created") } .doOnError { error -> showErrorMessage(error = error) } .subscribe() } ``` ```dart Flutter import 'dart:io'; Future createVideoStory(String communityId) async { final storyRepository = AmitySocialClient.newStoryRepository(); await storyRepository.createVideoStory( targetType: AmityStoryTargetType.COMMUNITY, targetId: communityId, videoFile: File('/path/to/video.mp4'), metadata: {'campaignId': 'launch'}, storyItems: [ HyperLink( url: 'https://example.com', customText: 'Watch more', ), ], ); } ``` ## Hyperlink items Story hyperlink items contain: | Field | Description | |-------|-------------| | `url` | Destination URL. | | `customText` | Optional text shown for the link. | ## Related topics Observe created stories and sync state. Mark synced stories as seen and track link clicks. --- ### [Get Stories](https://learn.social.plus/social-plus-sdk/social/content-management/stories/retrieval/get-stories) > Retrieve individual stories, active stories for one target, or stories across multiple targets. Story retrieval APIs return live objects or live collections. Use a single-story API when you already have a `storyId`, active-story APIs for a target's current stories, and multi-target APIs when building a story tray across communities. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Single story | `storyId` | Yes | Story ID to observe as a live object or stream. | | Active stories for a target | `targetType` | Yes | Story target type, such as community. | | Active stories for a target | `targetId` | Yes | Target ID whose active stories should be retrieved. | | Active stories for a target | Sort/order option | No | Sort order for returned active stories where exposed. | | Stories across targets | `targets` | Yes | Target type and target ID pairs to retrieve in one request. | | Stories across targets | Sort/order option | No | Sort order for returned stories where exposed. | ## Single Story Observe a single story when your UI already has a `storyId` and needs live story state. ```typescript TypeScript import { StoryRepository } from '@amityco/ts-sdk'; const unsubscribe = StoryRepository.getStoryByStoryId(storyId, ({ data, loading, error }) => { if (error) { handleError(error); return; } if (!loading && data) { updateUI(data); } }); unsubscribe(); ``` ```swift iOS token = storyRepository.getStory(storyId: "story-id").observe { object, error in if let error { handleError(error) return } if let story = object.snapshot { showSuccessMessage(story.storyId) } } ``` ```kotlin Android fun observeStory( storyRepository: AmityStoryRepository, storyId: String ) { storyRepository.getStory(storyId = storyId) .doOnNext { story: AmityStory -> showSuccessMessage(story.getStoryId()) } .doOnError { error -> showErrorMessage(error = error) } .subscribe() } ``` ```dart Flutter void observeStory(String storyId) { final stream = AmitySocialClient.newStoryRepository().live.getStory(storyId); stream.listen((story) { final currentStoryId = story.storyId; showError(currentStoryId ?? ''); }); } ``` ## Active Stories for a Target Active-story APIs retrieve non-expired stories for one target. They are the usual choice for rendering a viewer after the user opens a story ring. ```typescript TypeScript import { StoryRepository } from '@amityco/ts-sdk'; const unsubscribe = StoryRepository.getActiveStoriesByTarget( { targetType: 'community', targetId: communityId, options: { sortBy: 'createdAt', orderBy: 'desc', }, }, ({ data, loading, error }) => { if (error) { handleError(error); return; } if (!loading) { renderResults(data); } }, ); unsubscribe(); ``` ```swift iOS token = storyRepository .getActiveStoriesByTarget( targetType: .community, targetId: communityId, sortOption: .lastCreated ) .observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android fun observeActiveStories( storyRepository: AmityStoryRepository, targetId: String ) { storyRepository.getActiveStories( targetType = AmityStory.TargetType.COMMUNITY, targetId = targetId, sortOption = AmityStorySortOption.LAST_CREATED ) .doOnNext { stories: PagingData -> getPagingData(stories) } .doOnError { error -> showErrorMessage(error = error) } .subscribe() } ``` ```dart Flutter void observeActiveStories(String communityId) { final collection = StoryLiveCollection( request: () => AmitySocialClient.newStoryRepository() .getActiveStories( targetId: communityId, targetType: AmityStoryTargetType.COMMUNITY, orderBy: AmityStorySortingOrder.LAST_CREATED, ) .build(), ); collection.getStreamController().stream.listen((stories) { final visibleCount = stories.length; showError(visibleCount); }); collection.getData(); } ``` ## Stories Across Targets Use the multi-target APIs when you need stories from multiple targets in one live collection. ```typescript TypeScript import { StoryRepository } from '@amityco/ts-sdk'; const unsubscribe = StoryRepository.getStoriesByTargetIds( { targets: [ { targetType: 'community', targetId: communityId }, { targetType: 'community', targetId: 'community-id-2' }, ], options: { sortBy: 'createdAt', orderBy: 'desc', }, }, ({ data, loading, error }) => { if (error) { handleError(error); return; } if (!loading) { renderResults(data); } }, ); unsubscribe(); ``` ```swift iOS let targets = [ AmityStoryTargetSearchInfo(targetType: .community, targetId: communityId), AmityStoryTargetSearchInfo(targetType: .community, targetId: "community-id-2") ] token = storyRepository .getStoriesByTargets(targets: targets, sortOption: .lastCreated) .observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android fun observeStoriesByTargets(storyRepository: AmityStoryRepository) { val targets = listOf( AmityStory.TargetType.COMMUNITY to "community-id-1", AmityStory.TargetType.COMMUNITY to "community-id-2" ) storyRepository.getStoriesByTargets( targets = targets, sortOption = AmityStorySortOption.LAST_CREATED ) .doOnNext { stories: List -> showSuccessMessage(stories.size) } .doOnError { error -> showErrorMessage(error = error) } .subscribe() } ``` ```dart Flutter void observeStoriesByTargets() { final targets = [ StoryTargetSearchInfo( targetType: AmityStoryTargetType.COMMUNITY, targetId: 'community-id-1', ), StoryTargetSearchInfo( targetType: AmityStoryTargetType.COMMUNITY, targetId: 'community-id-2', ), ]; final collection = StoryLiveCollection( request: () => AmitySocialClient.newStoryRepository() .getStoriesByTargets( targets: targets, orderBy: AmityStorySortingOrder.LAST_CREATED, ) .build(), ); collection.getStreamController().stream.listen((stories) { final visibleCount = stories.length; showError(visibleCount); }); collection.getData(); } ``` ## Notes - Active-story APIs include local optimistic stories where the platform SDK supports optimistic creation. - Multi-target APIs return synced stories across the requested targets. - Use story target APIs when you only need ring state such as `hasUnseen`. ## Related Topics Retrieve target-level story availability and unseen state. Track views and reached users for stories. --- ### [Get Story Targets](https://learn.social.plus/social-plus-sdk/social/content-management/stories/retrieval/get-story-targets) > Retrieve story target state, including unseen status and local sync counts. Story target APIs let you render story rings without fetching every story. A story target tells you whether a target has unseen stories and, where supported, how many local stories are syncing or failed. The examples below use community targets. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Single story target | `targetType` | Yes | Story target type, such as community. | | Single story target | `targetId` | Yes | Target ID whose story state should be observed. | | Multiple story targets | `targets` | Yes | Target type and target ID pairs to observe together. | ## Single Story Target Observe one story target when rendering a story ring for a known community or user target. ```typescript TypeScript import { StoryRepository } from '@amityco/ts-sdk'; const unsubscribe = StoryRepository.getTargetById( { targetType: 'community', targetId: communityId, }, ({ data, loading, error }) => { if (error) { handleError(error); return; } if (!loading && data) { updateUI(data.hasUnseen); } }, ); unsubscribe(); ``` ```swift iOS token = storyRepository .getStoryTarget(targetType: .community, targetId: communityId) .observe { object, error in if let error { handleError(error) return } if let storyTarget = object.snapshot { showSuccessMessage(storyTarget.hasUnseen) } } ``` ```kotlin Android fun observeStoryTarget( storyRepository: AmityStoryRepository, targetId: String ) { storyRepository.getStoryTarget( targetType = AmityStory.TargetType.COMMUNITY, targetId = targetId ) .doOnNext { storyTarget: AmityStoryTarget -> showSuccessMessage(storyTarget.hasUnseen()) } .doOnError { error -> showErrorMessage(error = error) } .subscribe() } ``` ```dart Flutter void observeStoryTarget(String communityId) { final stream = AmitySocialClient.newStoryRepository().live.getStoryTaregt( targetType: AmityStoryTargetType.COMMUNITY, targetId: communityId, ); stream.listen((storyTarget) { final hasUnseen = storyTarget.hasUnseen; showError(hasUnseen); }); } ``` Flutter's single-target live helper is spelled `getStoryTaregt` in the current SDK. ## Multiple Story Targets Observe multiple story targets when building a tray that shows state for several targets at once. ```typescript TypeScript import { StoryRepository } from '@amityco/ts-sdk'; const unsubscribe = StoryRepository.getTargetsByTargetIds( [ { targetType: 'community', targetId: communityId }, { targetType: 'community', targetId: 'community-id-2' }, ], ({ data, loading, error }) => { if (error) { handleError(error); return; } if (!loading) { renderResults(data); } }, ); unsubscribe(); ``` ```swift iOS let targets = [ AmityStoryTargetSearchInfo(targetType: .community, targetId: communityId), AmityStoryTargetSearchInfo(targetType: .community, targetId: "community-id-2") ] token = storyRepository.getStoryTargets(targets: targets).observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android fun observeStoryTargets(storyRepository: AmityStoryRepository) { val targets = listOf( AmityStory.TargetType.COMMUNITY to "community-id-1", AmityStory.TargetType.COMMUNITY to "community-id-2" ) storyRepository.getStoryTargets(targets = targets) .doOnNext { storyTargets: List -> showSuccessMessage(storyTargets.size) } .doOnError { error -> showErrorMessage(error = error) } .subscribe() } ``` ```dart Flutter void observeStoryTargets() { final targets = [ StoryTargetSearchInfo( targetType: AmityStoryTargetType.COMMUNITY, targetId: 'community-id-1', ), StoryTargetSearchInfo( targetType: AmityStoryTargetType.COMMUNITY, targetId: 'community-id-2', ), ]; final collection = StoryTargetLiveCollection( request: () => AmitySocialClient.newStoryRepository() .getStoryTargets(targets: targets), ); collection.getStreamController().stream.listen((storyTargets) { final targetCount = storyTargets.length; showError(targetCount); }); collection.getData(); } ``` ## When to Use Story Targets - Use story targets for story tray rings, unseen indicators, and sync/error badges. - Use [Get Stories](./get-stories) when you need the actual story media and items. - Use [Get Global Story Targets](./get-global-story-targets) when you need a global discovery feed of active story targets. ## Related Topics Retrieve story objects and collections. Query story targets across the app. --- ### [Get Global Story Targets](https://learn.social.plus/social-plus-sdk/social/content-management/stories/retrieval/get-global-story-targets) > Retrieve active story targets across the app using global story target query options. Global story target APIs return story targets across the app. Use them for discovery experiences such as a global story tray. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Query global story targets | Seen-state query option | Yes | Option such as smart, unseen, seen, or all. | | Query global story targets | `limit` | No | TypeScript page size for global target results. | ## Query Options | Option | Description | |--------|-------------| | `SMART` | Combines unseen targets first, then seen targets. | | `UNSEEN` | Returns targets with unseen stories. | | `SEEN` | Returns targets without unseen stories. | | `ALL` | Returns all active story targets. | Option casing follows each platform: TypeScript uses `Amity.StorySeenQuery.SMART`, iOS uses `.smart`, Android and Flutter use `AmityGlobalStoryTargetsQueryOption.SMART`. ## Query Global Story Targets Query global story targets for a tray or discovery surface, using the seen-state option that matches your product sort order. ```typescript TypeScript import { StoryRepository } from '@amityco/ts-sdk'; const unsubscribe = StoryRepository.getGlobalStoryTargets( { seenState: Amity.StorySeenQuery.SMART, limit: 20, }, ({ data, loading, error, hasNextPage, onNextPage }) => { if (error) { handleError(error); return; } if (!loading) { renderResults(data); } if (hasNextPage) { onNextPage?.(); } }, ); unsubscribe(); ``` ```swift iOS token = storyRepository.getGlobalStoryTargets(option: .smart).observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android fun observeGlobalStoryTargets(storyRepository: AmityStoryRepository) { storyRepository.getGlobalStoryTargets( queryOption = AmityGlobalStoryTargetsQueryOption.SMART ) .doOnNext { targets: PagingData -> getPagingData(targets) } .doOnError { error -> showErrorMessage(error = error) } .subscribe() } ``` ```dart Flutter void observeGlobalStoryTargets() { final collection = GlobalStoryTargetLiveCollection( queryOption: AmityGlobalStoryTargetsQueryOption.SMART, ); collection.getStreamController().stream.listen((storyTargets) { final targetCount = storyTargets.length; showError(targetCount); }); collection.getData(); } ``` ## Notes - `SMART` is the right default for a consumer-facing global tray because it prioritizes unseen targets. - Use `UNSEEN` for a strict "new stories only" surface. - Use `SEEN` when you need a replay or archive-style surface of already seen active targets. - Use `ALL` when your UI needs all active targets regardless of current user's seen state. ## Related Topics Retrieve specific story target state. Retrieve the actual story objects for one or more targets. --- ### [Delete Story](https://learn.social.plus/social-plus-sdk/social/content-management/stories/actions/delete-story) > Soft delete or hard delete stories with StoryRepository deletion APIs. Story repositories expose soft delete and hard delete operations. Soft delete marks a story as deleted and removes it from normal story collections. Hard delete passes the permanent-delete flag to the backend. Treat hard delete as irreversible from the client perspective. Do not offer it in user-facing flows without confirmation. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Soft delete | `storyId` | Yes | Story ID to soft delete. | | Hard delete | `storyId` | Yes | Story ID to hard delete after confirmation. | ## Soft Delete Soft delete a story when the item should disappear from normal story surfaces while preserving deleted-state semantics. ```typescript TypeScript import { StoryRepository } from '@amityco/ts-sdk'; const deleted = await StoryRepository.softDeleteStory(storyId); showSuccessMessage(deleted); ``` ```swift iOS try await storyRepository.softDeleteStory(storyId: "story-id") showSuccessMessage("Story soft deleted") ``` ```kotlin Android fun softDeleteStory( storyRepository: AmityStoryRepository, storyId: String ) { storyRepository.softDeleteStory(storyId = storyId) .doOnComplete { showSuccessMessage("Story soft deleted") } .doOnError { error -> showErrorMessage(error = error) } .subscribe() } ``` ```dart Flutter Future softDeleteStory(String storyId) async { await AmitySocialClient.newStoryRepository().softDeleteStory( storyId: storyId, ); } ``` ## Hard Delete Hard delete a story only after the product confirms permanent removal is intended. ```typescript TypeScript import { StoryRepository } from '@amityco/ts-sdk'; const deleted = await StoryRepository.hardDeleteStory(storyId); showSuccessMessage(deleted); ``` ```swift iOS try await storyRepository.hardDeleteStory(storyId: "story-id") showSuccessMessage("Story hard deleted") ``` ```kotlin Android fun hardDeleteStory( storyRepository: AmityStoryRepository, storyId: String ) { storyRepository.hardDeleteStory(storyId = storyId) .doOnComplete { showSuccessMessage("Story hard deleted") } .doOnError { error -> showErrorMessage(error = error) } .subscribe() } ``` ```dart Flutter Future hardDeleteStory(String storyId) async { await AmitySocialClient.newStoryRepository().hardDeleteStory( storyId: storyId, ); } ``` ## Choosing a Delete Type | Delete type | Use when | |-------------|----------| | Soft delete | You want the story removed from normal story surfaces while preserving delete semantics in SDK state. | | Hard delete | You need a permanent deletion request and have confirmed the action with the user or moderator. | ## Related Topics Observe collections after delete operations. Create new story content. --- ### [Story Impressions](https://learn.social.plus/social-plus-sdk/social/content-management/stories/analytics/story-impressions) > Mark synced stories as seen, mark story link clicks, and query reached users. Story objects expose `impression`, `reach`, and `isSeen` state. Analytics helpers let you mark a synced story as seen and mark a story hyperlink as clicked. Call story analytics methods on synced story objects. Native and Flutter SDKs explicitly ignore unsynced stories for these analytics calls. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Mark story as seen | Synced story object | Yes | Story object whose analytics helper records the seen event. | | Mark story link as clicked | Synced story object | Yes | Story object whose analytics helper records the link-click event. | | Query reached users | `viewId` / `viewedId` | Yes | Story ID used to query reached users. | | Query reached users | `viewedType` | Yes | Viewed type value for stories. | | Query reached users | `limit` | No | TypeScript page size for reached-user results. | ## Mark Story as Seen Mark a synced story as seen after your UI decides the current user has viewed it. ```typescript TypeScript import { StoryRepository } from '@amityco/ts-sdk'; const unsubscribe = StoryRepository.getStoryByStoryId(storyId, ({ data, error }) => { if (error) { handleError(error); return; } data?.analytics.markAsSeen(); }); unsubscribe(); ``` ```swift iOS token = storyRepository.getStory(storyId: "story-id").observe { object, error in if let error { handleError(error) return } object.snapshot?.analytics.markAsSeen() } ``` ```kotlin Android fun markStoryAsSeen( storyRepository: AmityStoryRepository, story: AmityStory ) { storyRepository.analytics(story).markAsSeen() } ``` ```dart Flutter void markStoryAsSeen(AmityStory story) { AmitySocialClient.newStoryRepository().analytics(story).markAsSeen(); } ``` ## Mark Story Link as Clicked Mark a story link as clicked when the user opens the hyperlink item from the story UI. ```typescript TypeScript import { StoryRepository } from '@amityco/ts-sdk'; const unsubscribe = StoryRepository.getStoryByStoryId(storyId, ({ data, error }) => { if (error) { handleError(error); return; } data?.analytics.markLinkAsClicked(); }); unsubscribe(); ``` ```swift iOS token = storyRepository.getStory(storyId: "story-id").observe { object, error in if let error { handleError(error) return } object.snapshot?.analytics.markLinkAsClicked() } ``` ```kotlin Android fun markStoryLinkAsClicked( storyRepository: AmityStoryRepository, story: AmityStory ) { storyRepository.analytics(story).markLinkAsClicked() } ``` ```dart Flutter void markStoryLinkAsClicked(AmityStory story) { AmitySocialClient.newStoryRepository().analytics(story).markLinkAsClicked(); } ``` ## Query Reached Users Use reached-user APIs to retrieve users who viewed a story. ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; const unsubscribe = UserRepository.getReachedUsers( { viewId: storyId, viewedType: 'story', limit: 20, }, ({ data, loading, error, hasNextPage, onNextPage }) => { if (error) { handleError(error); return; } if (!loading) { renderResults(data); } if (hasNextPage) { onNextPage?.(); } }, ); unsubscribe(); ``` ```swift iOS token = userRepository .getReachedUsers(viewedType: .story, viewedId: "story-id") .observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android fun queryStoryReachedUsers( userRepository: AmityUserRepository, storyId: String ) { userRepository.getReachedUsers( viewedType = AmityViewedType.STORY, viewedId = storyId ) .doOnNext { users: PagingData -> getPagingData(users) } .doOnError { error -> showErrorMessage(error = error) } .subscribe() } ``` ```dart Flutter Future queryStoryReachedUsers(String storyId) async { final users = await AmityCoreClient.newUserRepository() .getViewedUsers( viewedType: AmityViewedType.STORY, viewedId: storyId, ) .query(); showError(users.length); } ``` ## Related Topics Retrieve stories before recording analytics events. Create stories with hyperlink items. ## Social — Discovery & Feed ### [Discovery & Engagement Overview](https://learn.social.plus/social-plus-sdk/social/discovery-engagement/overview) > SDK surfaces for feeds, search, notification tray, and discovery-driven engagement. Discovery and engagement APIs help users find content, communities, and activity updates. Use this overview to choose between feed retrieval, intelligent search, and notification tray APIs. Query global, custom-ranking, For You (personalized), community, and user feeds. Search posts and communities through SDK-supported semantic search surfaces. Retrieve tray items, item status, tray seen status, and notification event metadata. Add engagement affordances on posts, comments, stories, and other supported content. ## Choose The Right Surface | Goal | Use | | --- | --- | | Build a scrollable discovery feed | [Feed Overview](./feed/overview) | | Search communities by meaning or keyword | [Intelligent Search Community](./search/intelligent-search-community) | | Search posts or hashtags | [Intelligent Search Post](./search/intelligent-search-post) | | Show notifications in-app | [Notification Items](./notifications/notification-items) | | Track tray seen/unseen state | [Notification Tray Status](./notifications/notification-tray-status) | Platform coverage varies by surface. Each linked page includes the platform-specific SDK availability and code examples for that feature. ## Related Topics Query feed content for discovery surfaces. Compare post and community search options. Build notification tray experiences. Share content into feeds and engagement flows. --- ### [Feeds & Timelines](https://learn.social.plus/social-plus-sdk/social/discovery-engagement/feed/overview) > Query user, community, global, and custom-ranking global feeds with the SDK feed repository. Use `AmityFeedRepository` to read feed-style post collections. The SDK exposes user feeds, community feeds, global feed, and custom-ranking global feed. Global feed and custom-ranking global feed are separate SDK entry points; the backend owns ranking behavior, and the client SDK selects which feed endpoint to query. ## Feed APIs | Feed | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Global feed | `FeedRepository.getGlobalFeed()` | `AmityFeedRepository().getGlobalFeed()` | `AmitySocialClient.newFeedRepository().getGlobalFeed()` | `AmitySocialClient.newFeedRepository().getGlobalFeed()` | | Custom-ranking global feed | `FeedRepository.getCustomRankingGlobalFeed()` | `AmityFeedRepository().getCustomRankingGlobalFeed()` | `AmitySocialClient.newFeedRepository().getCustomRankingGlobalFeed()` | `AmitySocialClient.newFeedRepository().getCustomRankingGlobalFeed()` | | For You feed | `FeedRepository.getForYouFeed()` | Supported | Supported | Not available | | User feed | `FeedRepository.getUserFeed()` | `AmityFeedRepository().getUserFeed()` | `AmitySocialClient.newFeedRepository().getUserFeed()` | `AmitySocialClient.newFeedRepository().getUserFeed()` | | Community feed | `FeedRepository.getCommunityFeed()` | `AmityFeedRepository().getCommunityFeed()` | `AmitySocialClient.newFeedRepository().getCommunityFeed()` | `AmitySocialClient.newFeedRepository().getCommunityFeed()` | TypeScript still exports `queryGlobalFeed()`, but the SDK source marks it deprecated. Use the live collection APIs for new integrations. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Query global feed | `dataTypes` | No | Filter the global feed to specific post content types where supported. | | Query global feed | `includeMixedStructure` | No | Include mixed-structure posts alongside the requested data type filters. | | Query custom-ranking global feed | `includeMixedStructure` | No | Include mixed-structure posts in the backend-ranked global feed response. | | TypeScript pagination | `limit`, `onNextPage`, `hasNextPage` | No | Control page size and load additional pages from the live collection callback. | | Android refresh | `invalidateCache` | No | Skip cached paging data for deliberate refresh flows. | | Query For You feed | None | — | `getForYouFeed()` takes no query parameters. Pagination is handled through the live collection callback. | ## Query Global Feed Global feed returns a paginated collection of posts for the current user's global feed surface. Use data type filters when your UI only needs a subset of post types, such as an image or video feed. ```typescript TypeScript import { FeedRepository } from '@amityco/ts-sdk'; let loadNextPage: (() => void) | undefined; let canLoadMore = false; const unsubscribe = FeedRepository.getGlobalFeed( { dataTypes: ['image', 'video'], includeMixedStructure: true, limit: 20, }, ({ data: posts, onNextPage, hasNextPage, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(posts); loadNextPage = onNextPage; canLoadMore = hasNextPage; }, ); function loadMoreGlobalFeed() { if (canLoadMore) { loadNextPage?.(); } } unsubscribe(); ``` ```swift iOS token = feedRepository .getGlobalFeed(dataTypes: Set(["image", "video"]), includeMixedStructure: true) .observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android AmitySocialClient.newFeedRepository() .getGlobalFeed() .dataTypes(listOf(AmityPost.DataType.IMAGE, AmityPost.DataType.VIDEO)) .includeMixedStructure(includeMixedStructure = true) .build() .query() .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final page = await AmitySocialClient.newFeedRepository() .getGlobalFeed() .types([AmityDataType.IMAGE, AmityDataType.VIDEO]) .getPagingData(limit: 20); final posts = page.data; showError(posts.length); ``` ## Query Custom-Ranking Global Feed Use custom-ranking global feed when your app has enabled the backend-ranked global feed experience. The SDK does not expose ranking weights or formulas; it only requests the custom-ranking feed and returns the posts provided by the backend. ```typescript TypeScript import { FeedRepository } from '@amityco/ts-sdk'; let loadNextPage: (() => void) | undefined; let canLoadMore = false; const unsubscribe = FeedRepository.getCustomRankingGlobalFeed( { includeMixedStructure: true, limit: 20, }, ({ data: posts, onNextPage, hasNextPage, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(posts); loadNextPage = onNextPage; canLoadMore = hasNextPage; }, ); function loadMoreCustomRankingFeed() { if (canLoadMore) { loadNextPage?.(); } } unsubscribe(); ``` ```swift iOS token = feedRepository .getCustomRankingGlobalFeed(includeMixedStructure: true) .observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android AmitySocialClient.newFeedRepository() .getCustomRankingGlobalFeed() .includeMixedStructure(includeMixedStructure = true) .build() .query() .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ```dart Flutter final page = await AmitySocialClient.newFeedRepository() .getCustomRankingGlobalFeed() .getPagingData(limit: 20); final posts = page.data; showError(posts.length); ``` ## Query For You Feed For You feed is a backend-personalized global feed for the current user. Ranking is owned by the backend; the SDK only requests the feed and returns the posts the backend provides. It is a network-level feature that must be enabled for your network before the feed returns content. For You feed is a network setting. Read `getForYouFeedSetting()` on the client and only render the surface when the feed is enabled. If the feed is not enabled, the collection reports a feed-disabled error rather than an empty list — `AmityForYouFeedDisabledError` on TypeScript and Android, or the `.forYouFeedDisabled` error code on iOS (see below). `getForYouFeed()` is a live collection that takes **no query parameters**. Page through results with the `onNextPage` / `hasNextPage` values from the callback; the default page size is 20 posts. ```typescript TypeScript import { Client, FeedRepository } from '@amityco/ts-sdk'; // 1. Gate the surface on the network setting. const { forYouFeed } = await Client.getForYouFeedSetting(); if (!forYouFeed.enabled) { hideForYouSurface(); return; } // 2. Observe the personalized feed. let loadNextPage: (() => void) | undefined; let canLoadMore = false; const unsubscribe = FeedRepository.getForYouFeed( ({ data: posts, onNextPage, hasNextPage, loading, error }) => { if (loading) return; if (error) { if (error instanceof FeedRepository.AmityForYouFeedDisabledError) { // For You feed is not enabled for this network — hide the tab. hideForYouSurface(); return; } handleError(error); return; } renderResults(posts); loadNextPage = onNextPage; canLoadMore = hasNextPage; }, ); function loadMoreForYouFeed() { if (canLoadMore) { loadNextPage?.(); } } unsubscribe(); ``` ```swift iOS let forYouFeed = feedRepository.getForYouFeed() token = forYouFeed.observe { collection, error in if let error { if error.isAmityErrorCode(.forYouFeedDisabled) { // For You feed is not enabled for this network — hide the tab. hideForYouSurface() return } handleError(error) return } showSuccessMessage(collection.snapshots.count) } forYouFeed.nextPage() ``` ```kotlin Android AmitySocialClient.newFeedRepository() .getForYouFeed() .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> if (error is AmityForYouFeedDisabledError) { // For You feed is not enabled for this network — hide the tab. hideForYouSurface() } else { handleGeneralError(error) } }, ) ``` For You feed is available on TypeScript, iOS, and Android. It is not available in the current Flutter SDK. ### Read the For You feed setting `getForYouFeedSetting()` is a method on the client that reports whether the network has For You feed enabled. Use it to decide whether to render a For You tab or entry point at all. ```typescript TypeScript import { Client } from '@amityco/ts-sdk'; const setting = await Client.getForYouFeedSetting(); // setting: { forYouFeed: { enabled: boolean } } if (setting.forYouFeed.enabled) { showForYouTab(); } ``` ```swift iOS let setting = try await client.getForYouFeedSetting() if setting.enabled { showForYouTab() } ``` ```kotlin Android AmityCoreClient.getForYouFeedSetting() .doOnSuccess { setting: AmityForYouFeedSetting -> if (setting.enabled) { showForYouTab() } } .doOnError { error -> handleGeneralError(error) } .subscribe() ``` ## Android Cache Invalidation Android feed builders expose `invalidateCache(true)`. Use it for deliberate refresh flows, such as pull-to-refresh, when the first page should not reuse the existing paging cache. ```kotlin Android AmitySocialClient.newFeedRepository() .getGlobalFeed() .invalidateCache(invalidateCache = true) .build() .query() .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` `invalidateCache` is an Android feed-query option in the current SDK surface reviewed for this page. ## Notes - Use post query APIs when you need a specific user or community feed with richer filters such as tags, review status, or deletion state. - Dispose live collection subscriptions, notification tokens, and stream subscriptions when the screen is destroyed. - Treat custom-ranking behavior as backend-owned. Do not hardcode ranking assumptions in client UI logic. ## Related Topics Query user and community post collections with more filters. Search posts semantically or by hashtag. --- ### [Overview](https://learn.social.plus/social-plus-sdk/social/discovery-engagement/search/overview) > Use SDK search APIs for semantic post search, hashtag post search, and semantic community search. The SDK search pages in this section cover the client-side APIs for finding posts and communities. Keep the mental model simple: search is requested through SDK repository methods, filters narrow the search scope, and the backend returns ranked results. Semantic search availability depends on your network configuration. Confirm that the feature is enabled for your network before building UI that depends on semantic search results. ## Search Surfaces | Surface | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Semantic community search | `CommunityRepository.semanticSearchCommunities()` | `AmityCommunityRepository().semanticSearchCommunities(options:)` | `AmitySocialClient.newCommunityRepository().semanticSearchCommunities()` | Not exposed in the current public Flutter SDK source reviewed for this page | | Semantic post search | `PostRepository.semanticSearchPosts()` | `AmityPostRepository().semanticSearchPosts(options:)` | `AmitySocialClient.newPostRepository().semanticSearchPosts()` | Not exposed in the current public Flutter SDK source reviewed for this page | | Hashtag post search | `PostRepository.searchPostsByHashtag()` | `AmityPostRepository().searchPostsByHashtag(options:)` | `AmitySocialClient.newPostRepository().searchPostsByHashtag()` | Not exposed in the current public Flutter SDK source reviewed for this page | ## Filters | Filter | Community search | Post search | | --- | --- | --- | | Search text | `query` | `query` for semantic search, `hashtags` for hashtag search | | Category IDs | Supported for community semantic search | Not applicable | | Tags | Supported for community semantic search | Use post query APIs for tag filters; hashtag search uses `hashtags` | | Membership status | Supported by TypeScript, iOS, and Android community semantic search | Not applicable | | Target scope | Not applicable | Semantic post search can scope to a user or community target on TypeScript, iOS, and Android | | Post data types | Not applicable | Supported for semantic post search; hashtag search support differs by platform | | Mixed structure | Not applicable | Supported by TypeScript, iOS, and Android post search APIs | ## Implementation Guides Find communities by semantic query, category, tag, and membership status Search posts by semantic query or by hashtag ## Notes - Do not assume a fixed client-visible scoring formula. The SDK APIs return ranked collections, but ranking details are owned by the backend service. - Debounce user-entered queries before starting a new search request. - Keep empty, loading, and error states separate in your UI because search results may legitimately be empty. - Dispose live collection subscriptions and notification tokens when the screen is destroyed. --- ### [Posts](https://learn.social.plus/social-plus-sdk/social/discovery-engagement/search/intelligent-search-post) > Search posts semantically or by hashtag with the SDK post repository APIs. Use post search when users need to find content across accessible post targets. The SDK exposes semantic post search for natural-language queries and hashtag post search for explicit hashtag matching on TypeScript, iOS, and Android. The current public Flutter SDK source reviewed for this page does not expose semantic post search or hashtag post search repository methods. ## Parameters | Parameter | TypeScript | iOS | Android | | --- | --- | --- | --- | | Query | `query` | `query` | `query` | | Target | `targetType`, `targetId` | `targetType`, `targetId` | `targetType`, `targetId` | | Data types | `dataTypes` | `dataTypes` | `postTypes` | | Parent-only matching | `matchingOnlyParentPost` | `matchingOnlyParentPost` | `matchingOnlyParentPosts` | | Mixed structure | `includeMixedStructure` | `includeMixedStructure` | `includeMixedStructure` | ## Semantic Search Posts Use semantic search when a natural-language query should match accessible posts for the requested target and data types. ```typescript TypeScript import { PostRepository } from '@amityco/ts-sdk'; let loadNextPage: (() => void) | undefined; let canLoadMore = false; const unsubscribe = PostRepository.semanticSearchPosts( { query: 'healthy breakfast ideas', targetType: 'community', targetId: communityId, dataTypes: ['text', 'image'], matchingOnlyParentPost: true, includeMixedStructure: false, limit: 20, }, ({ data: posts, onNextPage, hasNextPage, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(posts); loadNextPage = onNextPage; canLoadMore = hasNextPage; }, ); function loadMorePosts() { if (canLoadMore) { loadNextPage?.(); } } unsubscribe(); ``` ```swift iOS let options = AmityPostSemanticSearchOptions( query: "healthy breakfast ideas", targetId: communityId, targetType: .community, dataTypes: ["text", "image"], matchingOnlyParentPost: true, includeMixedStructure: false ) token = postRepository .semanticSearchPosts(options: options) .observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android AmitySocialClient.newPostRepository() .semanticSearchPosts( query = "healthy breakfast ideas", targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, postTypes = listOf(AmityPost.DataType.TEXT, AmityPost.DataType.IMAGE), matchingOnlyParentPosts = true, includeMixedStructure = false ) .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ## Hashtag Search Filters | Parameter | TypeScript | iOS | Android | | --- | --- | --- | --- | | Hashtags | `hashtags` | `hashtags` | `hashtags` | | Target | `targetType` | Not exposed | Not exposed | | Data types | `dataTypes` | `dataTypes` | `dataTypes` | | Parent-only matching | `matchingOnlyParentPost` | `matchingOnlyParentPost` | Not exposed in the snippet surface | | Mixed structure | `includeMixedStructure` | `includeMixedStructure` | `includeMixedStructure` | ## Hashtag Search Posts Hashtag search finds posts that contain one or more hashtags. Pass hashtag names without the `#` prefix. ```typescript TypeScript import { PostRepository } from '@amityco/ts-sdk'; let loadNextHashtagPage: (() => void) | undefined; let canLoadMoreHashtagPosts = false; const unsubscribe = PostRepository.searchPostsByHashtag( { targetType: 'community', hashtags: ['recipe'], dataTypes: ['image'], matchingOnlyParentPost: true, includeMixedStructure: false, limit: 20, }, ({ data: posts, onNextPage, hasNextPage, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(posts); loadNextHashtagPage = onNextPage; canLoadMoreHashtagPosts = hasNextPage; }, ); function loadMoreHashtagPosts() { if (canLoadMoreHashtagPosts) { loadNextHashtagPage?.(); } } unsubscribe(); ``` ```swift iOS let options = AmityPostHashtagSearchOptions( hashtags: ["recipe"], dataTypes: ["image"], matchingOnlyParentPost: true, includeMixedStructure: false ) token = postRepository .searchPostsByHashtag(options: options) .observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android AmitySocialClient.newPostRepository() .searchPostsByHashtag( hashtags = listOf("recipe"), dataTypes = listOf(AmityPost.DataType.IMAGE), includeMixedStructure = false ) .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ## Notes - For semantic post search, provide `targetType` and `targetId` together when scoping search to one user or community target. - Use parent-only matching when the UI should show top-level posts instead of child posts from mixed or threaded structures. - For hashtag search, TypeScript requires a `targetType`; iOS and Android expose hashtag search without a target type parameter. - Search results are permission-aware from the backend response, but your UI should still handle empty results and authorization errors. ## Related Topics Query live post collections by target, type, review state, tags, and pagination options. Query feed-style post collections for global, custom-ranking, user, and community feeds. Review SDK search surfaces for community and post discovery. --- ### [Communities](https://learn.social.plus/social-plus-sdk/social/discovery-engagement/search/intelligent-search-community) > Search communities semantically with category, tag, membership, and discoverable private community filters. Use community semantic search when a discovery experience should match a user's intent instead of only exact community names. The client SDK sends a query and optional filters, then observes or receives a paginated set of `AmityCommunity` results. ## Parameters | Parameter | TypeScript | iOS | Android | | --- | --- | --- | --- | | Query | `query` | `query` | `query` | | Category IDs | `categoryIds` | `categoryIds` | `categoryIds` | | Tags | `tags` | `tags` | `AmityTags` | | Membership status | `communityMembershipStatus` | `communityMembershipStatus` | `filter` | | Discoverable private communities | `includeDiscoverablePrivateCommunity` | `includeDiscoverablePrivateCommunity` | `includeDiscoverablePrivateCommunity` | The current public Flutter SDK source reviewed for this page does not expose a semantic community search repository method. ## Search Communities Search communities with a semantic query and optional category, tag, membership, or discoverable-private filters. ```typescript TypeScript import { CommunityRepository } from '@amityco/ts-sdk'; let loadNextPage: (() => void) | undefined; let canLoadMore = false; const unsubscribe = CommunityRepository.semanticSearchCommunities( { query: 'trail running', categoryIds: ['category-id'], tags: ['outdoors'], includeDiscoverablePrivateCommunity: true, limit: 20, }, ({ data: communities, onNextPage, hasNextPage, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(communities); loadNextPage = onNextPage; canLoadMore = hasNextPage; }, ); function loadMoreCommunities() { if (canLoadMore) { loadNextPage?.(); } } unsubscribe(); ``` ```swift iOS let options = AmityCommunitySemanticSearchOptions( query: "trail running", categoryIds: ["category-id"], tags: ["outdoors"], communityMembershipStatus: .all, includeDiscoverablePrivateCommunity: true ) token = communityRepository .semanticSearchCommunities(options: options) .observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android import com.amity.socialcloud.sdk.model.social.community.AmityCommunityMembershipStatusFilter AmitySocialClient.newCommunityRepository() .semanticSearchCommunities( query = "trail running", filter = AmityCommunityMembershipStatusFilter.ALL, tags = AmityTags(listOf("outdoors")), categoryIds = listOf("category-id"), includeDiscoverablePrivateCommunity = true ) .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ## Membership Status | Intent | iOS | Android | TypeScript | | --- | --- | --- | --- | | Search all visible communities | `.all` | `AmityCommunityMembershipStatusFilter.ALL` | Omit `communityMembershipStatus` for the default all-status search | | Search joined communities | `.member` | `AmityCommunityMembershipStatusFilter.MEMBER` | Use the SDK's typed membership-status value in app code | | Search communities the user has not joined | `.notMember` | `AmityCommunityMembershipStatusFilter.NOT_MEMBER` | Use the SDK's typed membership-status value in app code | ## Notes - Use category and tag filters when your discovery UI already knows the user's topic area. - Use `includeDiscoverablePrivateCommunity` only when your product intentionally exposes discoverable private communities in search results. - Search results are returned as live or paginated collections depending on platform, so keep pagination and disposal logic close to the screen that owns the search. ## Related Topics Query and search communities by membership, category, tags, keyword, and sort order. Retrieve categories used to organize community discovery. Review SDK search surfaces for community and post discovery. ## Social — Notifications ### [Notification Tray Overview](https://learn.social.plus/social-plus-sdk/social/discovery-engagement/notifications/overview) > Read notification tray items, tray seen status, and item seen status with the SDK notification tray APIs. The notification tray SDK APIs expose in-app notification tray data for the signed-in user. Use them to render notification items, show a tray-level unread indicator, mark the tray as seen, and mark individual tray items as seen. The current public Flutter SDK source reviewed for this page exposes push-notification registration and notification-settings APIs, but not notification tray item/status APIs. ## SDK Surfaces | Capability | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Query tray items | `notificationTray.getNotificationTrayItems()` | `client.notificationTray.getNotificationTrayItems()` | `AmityCoreClient.notificationTray().getNotificationTrayItems()` | Not exposed in the current public Flutter SDK source reviewed for this page | | Observe tray seen status | `notificationTray.getNotificationTraySeen()` | `client.notificationTray.getNotificationTraySeen()` | `AmityCoreClient.notificationTray().getNotificationTraySeen()` | Not exposed in the current public Flutter SDK source reviewed for this page | | Mark tray seen | `notificationTray.markTraySeen(lastSeenAt)` | `client.notificationTray.markSeen()` | `AmityCoreClient.notificationTray().markTraySeen()` | Not exposed in the current public Flutter SDK source reviewed for this page | | Mark item seen | `notificationTray.markItemsSeen([{ id, lastSeenAt }])` | `AmityNotificationTrayItem.markSeen()` | `AmityNotificationTrayItem.markSeen()` | Not exposed in the current public Flutter SDK source reviewed for this page | ## Data Model Notification tray items are returned with actor, target, reference, action, text, and seen-state data. Property names differ slightly by platform. | Concept | TypeScript | iOS | Android | | --- | --- | --- | --- | | Item ID | `_id` | `notificationId` | `getId()` | | Last occurred | `lastOccurredAt` | `lastOccurredAt` | `getLastOccurredAt()` | | Last seen | `lastSeenAt` | `lastSeenAt` | `getLastSeenAt()` | | Action type | `actionType` | `actionType` | `getActionType()` | | Category | `trayItemCategory` | `trayItemCategory` | `getTrayItemCategory()` | | Target | `targetId`, `targetType` | `targetId`, `targetType` | `getTargetId()`, `getTargetType()` | | Text | `text`, `templatedText` | `text`, `templatedText` | `getText()`, `getTemplatedText()` | | Linked users | `users` | `users` | `getUsers()` | | Item seen state | `isSeen` | `isSeen` | `isSeen()` | | Recent-state helper | `isRecent` | `isRecent` | `isRecent()` | Tray seen status is a separate object with the latest tray occurrence timestamp, latest tray seen timestamp, and a derived `isSeen` value. ## Typical Flow 1. Observe tray seen status to decide whether to show a notification indicator. 2. Query tray items when the user opens the notification tray. 3. Mark the tray as seen after the user views the tray. 4. Mark individual items as seen when the user views or opens a specific item. 5. Dispose observers or subscriptions when the owning screen is destroyed. ## Related Topics Query tray items and mark individual items as seen Observe and update tray-level seen status Reference action and category values returned on tray items --- ### [Notification Tray Items](https://learn.social.plus/social-plus-sdk/social/discovery-engagement/notifications/notification-items) > Query notification tray items and mark individual items as seen. Notification tray items are paginated notification records for the signed-in user. Use this API to render an in-app notification tray and then mark individual items as seen when the user opens or views them. ## Parameters | Operation | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | --- | | Query items | `limit` / pagination handle | No | TypeScript, iOS, Android | Page-size and pagination controls where supported by the platform. | | Mark item seen | Item ID | Yes | TypeScript | Notification item ID to pass into `markItemsSeen`. | | Mark item seen | `lastSeenAt` | Yes | TypeScript | ISO timestamp for when the item was seen. | | Mark item seen | Notification tray item object | Yes | iOS, Android | Item returned by the tray item query; call `markSeen()` on the object. | ## Query Items Query notification tray items for the signed-in user, keeping the pagination handle while the tray screen is active. ```typescript TypeScript import { notificationTray } from '@amityco/ts-sdk'; let loadNextPage: (() => void) | undefined; let canLoadMore = false; const unsubscribe = notificationTray.getNotificationTrayItems( { limit: 20 }, ({ data: items, onNextPage, hasNextPage, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(items); loadNextPage = onNextPage; canLoadMore = hasNextPage; }, ); function loadMoreNotificationItems() { if (canLoadMore) { loadNextPage?.(); } } unsubscribe(); ``` ```swift iOS token = client.notificationTray .getNotificationTrayItems() .observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android AmityCoreClient.notificationTray() .getNotificationTrayItems() .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ## Mark an Item Seen Mark an individual notification item seen after the user opens or views that item. ```typescript TypeScript import { notificationTray } from '@amityco/ts-sdk'; async function markNotificationItemSeen(notificationId: string) { await notificationTray.markItemsSeen([ { id: notificationId, lastSeenAt: new Date().toISOString(), }, ]); } ``` ```swift iOS token = client.notificationTray .getNotificationTrayItems() .observe { collection, error in if let error { handleError(error) return } guard let firstItem = collection.snapshots.first else { return } Task { @MainActor in do { try await firstItem.markSeen() } catch { handleError(error) } } } ``` ```kotlin Android fun markNotificationItemSeen(item: AmityNotificationTrayItem) { item.markSeen() .subscribe( { showSuccessMessage("seen") }, { error -> handleGeneralError(error) } ) } ``` ## Notes - TypeScript marks one or more items by ID through `markItemsSeen`. - iOS and Android expose `markSeen()` on the `AmityNotificationTrayItem` model returned by the tray item query. - Use the platform-specific item ID field when routing from a tray item to a target screen: TypeScript `_id`, iOS `notificationId`, and Android `getId()`. - Dispose live collection subscriptions or notification tokens when the notification tray screen is destroyed. ## Related Topics Manage overall notification tray seen status. Reference action and category values returned on tray items. --- ### [Notification Tray Status](https://learn.social.plus/social-plus-sdk/social/discovery-engagement/notifications/notification-tray-status) > Observe and update notification tray seen status for unread indicators. Notification tray status tracks whether the signed-in user's tray has been seen since the latest tray item occurred. Use it for tray badges, unread indicators, and "new notifications" affordances. ## Status Fields | Concept | TypeScript | iOS | Android | | --- | --- | --- | --- | | Tray is seen | `isSeen` | `isSeen` | `isSeen()` | | Last tray occurrence | `lastTrayOccurredAt` | `lastTrayOccurredAt` | `getLastOccurredAt()` | | Last tray seen timestamp | `lastTraySeenAt` | `lastTraySeenAt` | `getLastSeenAt()` | The current public Flutter SDK source reviewed for this page does not expose notification tray seen-status APIs. ## Parameters | Operation | Input | Required | Platforms | Description | | --- | --- | --- | --- | --- | | Observe tray seen status | Signed-in user session | Yes | TypeScript, iOS, Android | The SDK returns status for the current user; no explicit user ID is passed. | | Mark tray seen | Seen timestamp | Platform-dependent | TypeScript | Timestamp to store as the tray seen time. | | Mark tray seen | Current tray context | Yes | iOS, Android | Marks the signed-in user's tray seen without an explicit timestamp parameter. | ## Observe Tray Seen Status Observe tray seen status when your app needs a badge or "new notifications" indicator to stay current. ```typescript TypeScript import { notificationTray } from '@amityco/ts-sdk'; const unsubscribe = notificationTray.getNotificationTraySeen( ({ data: status, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } updateUI(status); }, ); unsubscribe(); ``` ```swift iOS token = client.notificationTray .getNotificationTraySeen() .observe { object, error in if let error { handleError(error) return } if let status = object.snapshot { showSuccessMessage(status.isSeen) } } ``` ```kotlin Android AmityCoreClient.notificationTray() .getNotificationTraySeen() .subscribe( { status: AmityNotificationTraySeen -> showSuccessMessage(status.isSeen() == true) }, { error -> handleGeneralError(error) } ) ``` ## Mark Tray Seen Mark the tray seen after the user actually views the notification tray. ```typescript TypeScript import { notificationTray } from '@amityco/ts-sdk'; async function markNotificationTraySeen() { await notificationTray.markTraySeen(new Date().toISOString()); } ``` ```swift iOS Task { @MainActor in do { try await client.notificationTray.markSeen() } catch { handleError(error) } } ``` ```kotlin Android AmityCoreClient.notificationTray() .markTraySeen() .subscribe( { showSuccessMessage("seen") }, { error -> handleGeneralError(error) } ) ``` ## Notes - Mark the tray as seen when the user actually views the tray, not merely when a badge is rendered. - Keep tray-level status separate from item-level status. Marking the tray seen does not replace item-level interaction tracking in your UI. - Re-observe or refresh tray status when the app returns to the foreground if your badge must reflect cross-device activity. ## Related Topics Query notification items and mark individual items as seen. Understand the notification tray SDK surface. --- ### [Notification Event Fields](https://learn.social.plus/social-plus-sdk/social/discovery-engagement/notifications/notification-events-reference) > Reference action and category values returned on notification tray items. Notification tray items include string fields that identify what happened and what kind of tray item was produced. Use these fields for routing, icons, grouping, and display decisions in your app. Do not hardcode notification message-template behavior from this page. The SDK returns `text` and `templatedText` on each tray item. Render those fields or map action/category values only for UI behavior you own, such as icons and routing. ## Core Fields | Field | TypeScript | iOS | Android | Purpose | | --- | --- | --- | --- | --- | | Action type | `actionType` | `actionType` | `getActionType()` | Broad action family | | Tray item category | `trayItemCategory` | `trayItemCategory` | `getTrayItemCategory()` | More specific category for mentions, reactions, follows, events, and related actions | | Target | `targetId`, `targetType` | `targetId`, `targetType` | `getTargetId()`, `getTargetType()` | Object the notification points at | | Reference | `referenceId`, `referenceType` | `referenceId`, `referenceType` | `getReferenceId()`, `getReferenceType()` | Related object, when present | | Parent | `parentId` | `parentId` | `getParentId()` | Parent object, when present | | Rendered text | `text`, `templatedText` | `text`, `templatedText` | `getText()`, `getTemplatedText()` | Text returned by the backend for display | ## Action Types The TypeScript SDK type surface defines these action values. iOS and Android expose action type as a string on each returned tray item. | Value | Meaning | | --- | --- | | `post` | Post-related notification | | `poll` | Poll-related notification | | `comment` | Comment-related notification | | `reaction` | Reaction-related notification | | `mention` | Mention-related notification | | `reply` | Reply-related notification | | `join_request` | Join-request-related notification | | `user` | User-related notification | ## Tray Item Categories The TypeScript SDK type surface defines these category values. iOS and Android expose the category as a string on each returned tray item. | Value | Meaning | | --- | --- | | `mention_in_post` | Mention in a post | | `mention_in_comment` | Mention in a comment | | `mention_in_reply` | Mention in a reply | | `mention_in_poll` | Mention in a poll | | `reaction_on_post` | Reaction on a post | | `reaction_on_comment` | Reaction on a comment | | `reaction_on_reply` | Reaction on a reply | | `respond_on_join_request` | Response to a join request | | `follow` | Follow notification | | `event_reminder` | Event reminder | | `event_started` | Event started notification | | `event_created` | Event created notification | | `room_cohost_invite` | Room co-host invitation | | `user_profile_reset` | User profile reset notification | ## Routing Guidance - Use `targetType` and `targetId` for the primary destination. - Use `referenceType` and `referenceId` when your UI needs to highlight a related post, comment, event, or other referenced object. - Use `rootId` and `latestCommentId` when they are present and your comment UI needs to open a thread context. - Prefer the returned `text` or `templatedText` for display copy instead of rebuilding notification copy in the client. ## Related Documentation Query tray items and inspect returned fields Observe and update tray-level seen status ## Social — Events ### [Events Overview](https://learn.social.plus/social-plus-sdk/social/events/overview) > Create, query, update, delete, and RSVP to scheduled events using the SDK event APIs. Events are scheduled social objects with a title, description, type, start and end time, origin, optional location or external URL, optional cover image, tags, metadata, and RSVP counts. The SDK event surface is currently exposed in TypeScript, iOS, and Android. The public Flutter SDK source reviewed for this page does not expose scheduled event creation, query, management, or RSVP APIs. ## Platform Coverage | Capability | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Create event | `EventRepository.createEvent(options)` | `AmityEventRepository().createEvent(options:)` | `AmitySocialClient.newEventRepository().createEvent()...build().create()` | Not exposed in the current public Flutter SDK source reviewed for this page | | Get event | `EventRepository.getEvent(eventId, callback)` | `AmityEventRepository().getEvent(id:)` | `AmitySocialClient.newEventRepository().getEvent(eventId)` | Not exposed | | Query events | `EventRepository.getEvents(params, callback)` | `AmityEventRepository().getEvents(options:)` | `AmitySocialClient.newEventRepository().getEvents()...build().query()` | Not exposed | | Query my RSVP events | `EventRepository.getMyEvents({ status }, callback)` | Use `AmityEventQueryOptions(userId:onlyAttendee:)` | Use `getEvents().userId(...).onlyAttendee(true)` | Not exposed | | Update event | `EventRepository.updateEvent(eventId, options)` | `AmityEventRepository().updateEvent(id:options:)` | `AmitySocialClient.newEventRepository().updateEvent(eventId)...build().apply()` | Not exposed | | Delete event | `EventRepository.deleteEvent(eventId)` | `AmityEventRepository().deleteEvent(id:)` | `AmitySocialClient.newEventRepository().deleteEvent(eventId)` | Not exposed | | RSVP | `event.createRSVP(...)`, `event.updateRSVP(...)`, `event.getMyRSVP()`, `event.getRSVPs(...)` | Same methods on `AmityEvent` | Same methods on `AmityEvent` | Not exposed | | Share event link | `Client.getShareableLinkConfiguration()` + `AmitySharableContentType.EVENT` | Supported | Supported | Not available | ## Event Fields Core fields returned by the SDK include: | Field | Notes | | --- | --- | | `eventId` | Stable event identifier. Android exposes it as `getEventId()`. | | `title`, `description` | Event copy supplied at creation time. | | `type` | `virtual` or `in_person`. | | `status` | TypeScript and iOS expose `scheduled`, `live`, `ended`, and `cancelled`. Android also exposes `DRAFT` and `UNKNOWN` enum values for server compatibility. | | `startTime`, `endTime` | Required when creating events. TypeScript accepts ISO strings; iOS uses `Date`; Android uses Joda `DateTime`. | | `originType`, `originId` | The event origin: `community` or `user`, plus the corresponding ID. | | `location`, `externalUrl`, `coverImageFileId` | Optional display and media fields. | | `tags`, `metadata` | Optional structured fields. Timezone can be stored in `metadata.timezone` on TypeScript/iOS and via `timezone(...)` on Android. | | `rsvpCount`, `interestedCount` | Count fields returned with the event. | | `creator`, `targetCommunity`, `coverImage`, `post`, `room` | Linked objects are present when returned by the SDK payload/cache. Availability depends on platform and response payload. | ## RSVP Statuses | Status | TypeScript | iOS | Android | | --- | --- | --- | --- | | Going | `AmityEventResponseStatus.Going` | `.going` | `AmityEventResponseStatus.GOING` | | Interested | Not exposed by the current TypeScript enum reviewed for this page | `.interested` | `AmityEventResponseStatus.INTERESTED` | | Not going | `AmityEventResponseStatus.NotGoing` | `.notGoing` | `AmityEventResponseStatus.NOT_GOING` | ## Common Flow 1. Create an event with a title, description, type, start time, end time, origin type, and origin ID. 2. Query events by origin, status, type, user ID, attendee filter, sort option, and order option. 3. Observe a single event if the detail screen needs live updates. 4. Let users RSVP through the methods on the returned `AmityEvent`. 5. Update or delete events through the event repository when the current user has permission. This page documents SDK method names and data surfaces only. Product policy such as who can create, edit, RSVP, or view an event is enforced by backend permissions and should be handled by your app UI and error handling. ## Event Guides Create scheduled virtual or in-person events. Get, query, update, and delete event objects. Create, update, read, and query RSVP responses. Generate a shareable deep link to an event. --- ### [Create Event](https://learn.social.plus/social-plus-sdk/social/events/create-event) > Create scheduled virtual or in-person events with the SDK event repository. Create events through the event repository on TypeScript, iOS, and Android. A create request needs a title, description, event type, start time, end time, origin type, and origin ID. Optional fields include location, external URL, cover image file ID, tags, and metadata. Flutter does not currently expose a public scheduled-event creation API in the SDK source reviewed for this page. ## Parameters | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | | `title` | Yes | TypeScript, iOS, Android | Event title shown to users. | | `description` | Yes | TypeScript, iOS, Android | Event description or summary. | | `type` | Yes | TypeScript, iOS, Android | Virtual or in-person event type. | | `startTime`, `endTime` | Yes | TypeScript, iOS, Android | Start and end time. TypeScript accepts ISO strings, iOS uses `Date`, and Android uses Joda `DateTime`. | | `originType`, `originId` | Yes | TypeScript, iOS, Android | Event owner scope, usually a community origin and community ID. | | `location` | No | TypeScript, iOS, Android | Physical location for in-person events. | | `externalUrl` | No | TypeScript, iOS, Android | Link for a virtual event or external event details. | | `coverImageFileId` | No | TypeScript, iOS, Android | Uploaded file ID for the event cover image. | | `tags` | No | TypeScript, iOS, Android | Tags used for app-owned grouping or filtering. | | `metadata` / `timezone` | No | TypeScript, iOS, Android | Custom metadata. Android exposes timezone through `.timezone(...)`. | ## Create an Event Use the create method when your app has collected the required event fields and knows the target origin. The examples below create a virtual community event and return the created event object. ```typescript TypeScript import { AmityEventOriginType, AmityEventType, EventRepository, } from '@amityco/ts-sdk'; const { data: event } = await EventRepository.createEvent({ title: 'Community Workshop', description: 'A live workshop for community members.', type: AmityEventType.Virtual, startTime: '2026-08-01T09:00:00.000Z', endTime: '2026-08-01T10:00:00.000Z', originType: AmityEventOriginType.Community, originId: communityId, externalUrl: 'https://example.com/live', tags: ['workshop'], metadata: { timezone: 'UTC' }, }); showSuccessMessage(event.eventId); ``` ```swift iOS let repository = AmityEventRepository() let options = AmityEventCreateOptions( title: "Community Workshop", description: "A live workshop for community members.", type: .virtual, startTime: Date(), endTime: Date().addingTimeInterval(3600), originType: .community, originId: communityId, externalUrl: "https://example.com/live", tags: ["workshop"], metadata: ["timezone": "UTC"] ) let event = try await repository.createEvent(options: options) showSuccessMessage(event.eventId) ``` ```kotlin Android AmitySocialClient.newEventRepository() .createEvent() .title("Community Workshop") .description("A live workshop for community members.") .type(AmityEventType.VIRTUAL) .startTime(DateTime.now().plusDays(7)) .endTime(DateTime.now().plusDays(7).plusHours(1)) .originType(AmityEventOriginType.COMMUNITY) .originId(communityId) .externalUrl("https://example.com/live") .timezone("UTC") .tags(listOf("workshop")) .build() .create() .subscribe( { event: AmityEvent -> showSuccessMessage(event.getEventId()) }, { error -> handleGeneralError(error) } ) ``` ## Platform Notes - TypeScript uses `AmityEventType.Virtual` and `AmityEventType.InPerson`. - iOS uses `.virtual` and `.inPerson`. - Android uses `AmityEventType.VIRTUAL` and `AmityEventType.IN_PERSON`. - Android requires `endTime(...)` before calling `build()`. ## Related Topics Get, query, update, and delete events. Create, update, read, and query RSVP responses. Review event fields, coverage, and common flows. --- ### [Manage Events](https://learn.social.plus/social-plus-sdk/social/events/manage-events) > Get, query, update, and delete events with SDK event repository APIs. Use event repository APIs to observe a single event, query event collections, update event fields, and delete events. Flutter does not currently expose public scheduled-event management APIs in the SDK source reviewed for this page. ## Parameters | Input | Methods | Platforms | Description | | --- | --- | --- | --- | | `eventId` | Get, update, delete | TypeScript, iOS, Android | Stable ID of the event to read or mutate. | | `originType`, `originId` | Query | TypeScript, iOS, Android | Scope events to a community or user origin. | | `userId`, `onlyAttendee` | Query my RSVP events | TypeScript, iOS, Android | Filter events by attendee or user context. | | `status` | Query | TypeScript, iOS, Android | Filter by scheduled, live, ended, or cancelled status. | | `type` | Query | TypeScript, iOS, Android | Filter by virtual or in-person event type. | | `sortBy`, `orderBy` | Query | TypeScript, iOS, Android | Sort event lists by start time or creation time. | | Event fields | Update | TypeScript, iOS, Android | Mutable fields such as title, external URL, description, location, tags, and metadata. | ## Get an Event Use this method for event detail screens or any flow that needs the latest state of one event. TypeScript and iOS expose observable objects; Android returns the requested event through the repository. ```typescript TypeScript import { EventRepository } from '@amityco/ts-sdk'; const unsubscribe = EventRepository.getEvent( 'event-123', ({ data: event, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } if (!event) return; showSuccessMessage(event.title); }, ); unsubscribe(); ``` ```swift iOS let repository = AmityEventRepository() token = repository.getEvent(id: "event-123").observe { eventObject, error in if let error { handleError(error) return } guard let event = eventObject.snapshot else { return } showSuccessMessage(event.title) } ``` ```kotlin Android AmitySocialClient.newEventRepository() .getEvent("event-123") .subscribe( { event: AmityEvent -> showSuccessMessage(event.getTitle()) }, { error -> handleGeneralError(error) } ) ``` ## Query Events Use event queries to build calendar lists, community event lists, and upcoming-event surfaces. Pass origin, status, type, sort, and order filters that match the list your UI needs. ```typescript TypeScript import { AmityEventOrderOption, AmityEventOriginType, AmityEventSortOption, AmityEventStatus, EventRepository, } from '@amityco/ts-sdk'; const unsubscribe = EventRepository.getEvents( { originType: AmityEventOriginType.Community, originId: communityId, status: AmityEventStatus.Scheduled, sortBy: AmityEventSortOption.StartTime, orderBy: AmityEventOrderOption.Ascending, }, ({ data: events, loading, error, hasNextPage, onNextPage }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(events); if (hasNextPage) { onNextPage?.(); } }, ); unsubscribe(); ``` ```swift iOS let repository = AmityEventRepository() let options = AmityEventQueryOptions( originType: .community, originId: communityId, status: .scheduled, sortBy: .startTime, orderBy: .ascending ) token = repository.getEvents(options: options).observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.map(\.title)) } ``` ```kotlin Android AmitySocialClient.newEventRepository() .getEvents() .originType(AmityEventOriginType.COMMUNITY) .originId(communityId) .status(AmityEventStatus.SCHEDULED) .sortBy(AmityEventSortOption.START_TIME) .orderBy(AmityEventOrderOption.ASCENDING) .build() .query() .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ## Query My RSVP Events Use this flow when the current screen should show events connected to a user's RSVP or attendance state. The SDKs expose this through event query filters rather than a separate event object method. ```typescript TypeScript import { AmityEventResponseStatus, EventRepository } from '@amityco/ts-sdk'; const unsubscribe = EventRepository.getMyEvents( { status: AmityEventResponseStatus.Going }, ({ data: events, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(events); }, ); unsubscribe(); ``` ```swift iOS let options = AmityEventQueryOptions( userId: "user-123", onlyAttendee: true, sortBy: .startTime, orderBy: .ascending ) token = AmityEventRepository().getEvents(options: options).observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```kotlin Android AmitySocialClient.newEventRepository() .getEvents() .userId(userId) .onlyAttendee(true) .sortBy(AmityEventSortOption.START_TIME) .orderBy(AmityEventOrderOption.ASCENDING) .build() .query() .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ## Update an Event Use update when the current user can edit the event and your app has collected the changed fields. The examples update the title and virtual-event URL. ```typescript TypeScript import { EventRepository } from '@amityco/ts-sdk'; const { data: event } = await EventRepository.updateEvent('event-123', { title: 'Updated Community Workshop', externalUrl: 'https://example.com/updated-live', }); showSuccessMessage(event.title); ``` ```swift iOS let repository = AmityEventRepository() let options = AmityEventUpdateOptions( title: "Updated Community Workshop", externalUrl: "https://example.com/updated-live" ) let event = try await repository.updateEvent(id: "event-123", options: options) showSuccessMessage(event.title) ``` ```kotlin Android AmitySocialClient.newEventRepository() .updateEvent("event-123") .title("Updated Community Workshop") .externalUrl("https://example.com/updated-live") .build() .apply() .subscribe( { event: AmityEvent -> showSuccessMessage(event.getTitle()) }, { error -> handleGeneralError(error) } ) ``` ## Delete an Event Use delete for event owner or moderator flows where removing the event is allowed by backend permissions. Handle errors in your UI because permission and lifecycle rules are enforced server-side. ```typescript TypeScript import { EventRepository } from '@amityco/ts-sdk'; await EventRepository.deleteEvent('event-123'); showSuccessMessage('Event deleted'); ``` ```swift iOS try await AmityEventRepository().deleteEvent(id: "event-123") showSuccessMessage("Event deleted") ``` ```kotlin Android AmitySocialClient.newEventRepository() .deleteEvent("event-123") .subscribe( { showSuccessMessage("Event deleted") }, { error -> handleGeneralError(error) } ) ``` ## Query Filters | Filter | TypeScript | iOS | Android | | --- | --- | --- | --- | | Origin | `originType`, `originId` | `originType`, `originId` | `.originType(...)`, `.originId(...)` | | Creator or attendee user | `userId`, `onlyAttendee` | `userId`, `onlyAttendee` | `.userId(...)`, `.onlyAttendee(...)` | | Status | `AmityEventStatus.Scheduled`, `Live`, `Ended`, `Cancelled` | `.scheduled`, `.live`, `.ended`, `.cancelled` | `AmityEventStatus.SCHEDULED`, `LIVE`, `ENDED`, `CANCELLED` | | Type | `AmityEventType.Virtual`, `InPerson` | `.virtual`, `.inPerson` | `AmityEventType.VIRTUAL`, `IN_PERSON` | | Sort | `AmityEventSortOption.StartTime`, `CreatedAt` | `.startTime`, `.createdAt` | `AmityEventSortOption.START_TIME`, `CREATED_AT` | | Order | `AmityEventOrderOption.Ascending`, `Descending` | `.ascending`, `.descending` | `AmityEventOrderOption.ASCENDING`, `DESCENDING` | TypeScript `deleteEvent` returns `void`. Android returns a `Completable`; iOS returns from an async throwing function when deletion completes. ## Related Topics Create scheduled virtual or in-person events. Work with RSVP responses on event objects. Review event fields, coverage, and common flows. --- ### [Event RSVP](https://learn.social.plus/social-plus-sdk/social/events/event-rsvp) > Create, update, read, and query event RSVP responses from SDK event objects. RSVP methods are exposed on the `AmityEvent` object returned by the event repository. Use them after you have loaded or created an event. Flutter does not currently expose public scheduled-event RSVP APIs in the SDK source reviewed for this page. ## Status Values | Meaning | TypeScript | iOS | Android | | --- | --- | --- | --- | | Going | `AmityEventResponseStatus.Going` | `.going` | `AmityEventResponseStatus.GOING` | | Interested | Not exposed by the current TypeScript enum reviewed for this page | `.interested` | `AmityEventResponseStatus.INTERESTED` | | Not going | `AmityEventResponseStatus.NotGoing` | `.notGoing` | `AmityEventResponseStatus.NOT_GOING` | ## Parameters | Parameter | Methods | Platforms | Description | | --- | --- | --- | --- | | `event` | All RSVP methods | TypeScript, iOS, Android | Loaded `AmityEvent` object that exposes RSVP methods. | | `status` | Create, update, query | TypeScript, iOS, Android | RSVP status to create, update, or filter. | | `eventId` | Load before RSVP | TypeScript, iOS, Android | Event ID used to retrieve the event object first. | | Pagination callback or collection | Query RSVPs | TypeScript, iOS, Android | Returned RSVP list or live collection for rendering attendees. | ## Create RSVP Create an RSVP when a user responds to an event for the first time. The examples load the event first, then call `createRSVP` on the returned event object. ```typescript TypeScript import { AmityEventResponseStatus, EventRepository } from '@amityco/ts-sdk'; const unsubscribe = EventRepository.getEvent( 'event-123', async ({ data: event, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } if (!event) return; const rsvp = await event.createRSVP(AmityEventResponseStatus.Going); showSuccessMessage(rsvp?.status); }, ); unsubscribe(); ``` ```swift iOS token = AmityEventRepository().getEvent(id: "event-123").observe { eventObject, error in if let error { handleError(error) return } guard let event = eventObject.snapshot else { return } Task { do { let rsvp = try await event.createRSVP(status: .going) showSuccessMessage(rsvp.status.rawValue) } catch { handleError(error) } } } ``` ```kotlin Android AmitySocialClient.newEventRepository() .getEvent("event-123") .firstOrError() .flatMap { event: AmityEvent -> event.createRSVP(AmityEventResponseStatus.GOING) } .subscribe( { rsvp: AmityEventResponse -> showSuccessMessage(rsvp.getStatus()) }, { error -> handleGeneralError(error) } ) ``` ## Update RSVP Update an RSVP when a user changes their response. Use the same event object flow, but pass the new status to `updateRSVP`. ```typescript TypeScript import { AmityEventResponseStatus, EventRepository } from '@amityco/ts-sdk'; const unsubscribe = EventRepository.getEvent( 'event-123', async ({ data: event, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } if (!event) return; const rsvp = await event.updateRSVP(AmityEventResponseStatus.NotGoing); showSuccessMessage(rsvp?.status); }, ); unsubscribe(); ``` ```swift iOS token = AmityEventRepository().getEvent(id: "event-123").observe { eventObject, error in if let error { handleError(error) return } guard let event = eventObject.snapshot else { return } Task { do { let rsvp = try await event.updateRSVP(status: .notGoing) showSuccessMessage(rsvp.status.rawValue) } catch { handleError(error) } } } ``` ```kotlin Android AmitySocialClient.newEventRepository() .getEvent("event-123") .firstOrError() .flatMap { event: AmityEvent -> event.updateRSVP(AmityEventResponseStatus.NOT_GOING) } .subscribe( { rsvp: AmityEventResponse -> showSuccessMessage(rsvp.getStatus()) }, { error -> handleGeneralError(error) } ) ``` ## Get My RSVP Get the active user's RSVP for an event when the UI needs to show the current response before offering update actions. ```typescript TypeScript import { EventRepository } from '@amityco/ts-sdk'; const unsubscribe = EventRepository.getEvent( 'event-123', async ({ data: event, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } if (!event) return; const rsvp = await event.getMyRSVP(); showSuccessMessage(rsvp?.status); }, ); unsubscribe(); ``` ```swift iOS token = AmityEventRepository().getEvent(id: "event-123").observe { eventObject, error in if let error { handleError(error) return } guard let event = eventObject.snapshot else { return } Task { do { let rsvp = try await event.getMyRSVP() showSuccessMessage(rsvp.status.rawValue) } catch { handleError(error) } } } ``` ```kotlin Android AmitySocialClient.newEventRepository() .getEvent("event-123") .firstOrError() .flatMap { event: AmityEvent -> event.getMyRSVP() } .subscribe( { rsvp: AmityEventResponse -> showSuccessMessage(rsvp.getStatus()) }, { error -> handleGeneralError(error) } ) ``` ## Query RSVPs Query RSVP responses when you need to render attendee lists, counts by status, or moderation views. Filter by status when the screen only needs one response group. ```typescript TypeScript import { AmityEventResponseStatus, EventRepository } from '@amityco/ts-sdk'; let rsvpUnsubscribe: Amity.Unsubscriber | undefined; const unsubscribe = EventRepository.getEvent( 'event-123', ({ data: event, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } if (!event) return; rsvpUnsubscribe = event.getRSVPs( { status: AmityEventResponseStatus.Going }, ({ data: responses, loading, error }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(responses); }, ); }, ); rsvpUnsubscribe?.(); unsubscribe(); ``` ```swift iOS token = AmityEventRepository().getEvent(id: "event-123").observe { eventObject, error in if let error { handleError(error) return } guard let event = eventObject.snapshot else { return } token = event.getRSVPs(status: .going).observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } } ``` ```kotlin Android AmitySocialClient.newEventRepository() .getEvent("event-123") .firstOrError() .flatMapPublisher { event: AmityEvent -> event.getRSVPs(AmityEventResponseStatus.GOING) } .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) } ) ``` ## Response Fields `AmityEventResponse` includes the event ID, user ID, status, linked user when available, creation time, update time, and the time the user responded. Android also exposes an RSVP ID through `getRsvpId()`. ## Related Topics Create the event before collecting RSVP responses. Get and query event objects before calling RSVP methods. Review event coverage, fields, and status values. ## Social — Follow & User Relationships ### [Follow/Unfollow User](https://learn.social.plus/social-plus-sdk/social/user-relationship/following/follow-unfollow-user) > Follow or unfollow another user with the social.plus SDKs. Use the follow API to create a relationship from the current user to another user. The SDK returns the resulting follow status where the platform exposes one. Depending on network configuration, the status can become `accepted` immediately or remain `pending` until the target user accepts the request. Use the unfollow API to cancel a pending request or remove an accepted follow relationship. If the relationship is blocked, follow and unfollow calls can fail or return a blocked status. Read the connection status before rendering a follow button when the UI needs to distinguish blocked users. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Follow user | `userId` / `targetUserId` | Yes | ID of the user the current user wants to follow. | | Unfollow user | `userId` / `targetUserId` | Yes | ID of the user the current user wants to unfollow or whose pending request should be canceled. | ## Follow User Follow a user when the current user starts a relationship that may become accepted immediately or remain pending. ```swift iOS let relationship = AmityUserRelationship() let response = try await relationship.follow(withUserId: "target-user-id") _ = response ``` ```kotlin Android val disposable = AmityCoreClient.newUserRepository() .relationship() .follow(userId = targetUserId) .subscribe( { status: AmityFollowStatus -> showSuccessMessage(status.apiKey) }, { error -> handleGeneralError(error) }, ) ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; const { data: followStatus } = await UserRepository.Relationship.follow(userId); if (followStatus.status === 'accepted') { showSuccessMessage('Following'); } ``` ```dart Flutter final status = await AmityCoreClient.newUserRepository() .relationship() .follow(targetUserId); final statusValue = status.value; ``` ## Unfollow User Unfollow a user to remove an accepted relationship or cancel a pending follow request. ```swift iOS let relationship = AmityUserRelationship() let response = try await relationship.unfollow(withUserId: "target-user-id") _ = response ``` ```kotlin Android val disposable = AmityCoreClient.newUserRepository() .relationship() .unfollow(userId = targetUserId) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; const didUnfollow = await UserRepository.Relationship.unfollow(userId); if (didUnfollow) { showSuccessMessage('Unfollowed'); } ``` ```dart Flutter final status = await AmityCoreClient.newUserRepository() .relationship() .unfollow(targetUserId); final statusValue = status.value; ``` ## Related Topics Handle incoming follow requests. Read status and counters. Query followers and following. --- ### [Accept/Decline Follow Request](https://learn.social.plus/social-plus-sdk/social/user-relationship/following/accept-decline-follow-request) > Accept or decline incoming follow requests with the social.plus SDKs. When follow approval is enabled, a follow call creates a pending request. The target user can accept the request to make the relationship active, or decline it to remove the request. Accept and decline methods operate on requests received by the current user. If the request has already been withdrawn, accepted, or declined, handle the SDK error and refresh the request list or follow info. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Accept follow request | `userId` / `targetUserId` | Yes | ID of the requester whose incoming follow request should be accepted. | | Decline follow request | `userId` / `targetUserId` | Yes | ID of the requester whose incoming follow request should be declined. | ## Accept Follow Request Accept an incoming follow request when the current user approves the requester. ```swift iOS let relationship = AmityUserRelationship() let response = try await relationship.acceptMyFollower(withUserId: "requester-user-id") _ = response ``` ```kotlin Android val disposable = AmityCoreClient.newUserRepository() .relationship() .acceptMyFollower(userId = targetUserId) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; const didAccept = await UserRepository.Relationship.acceptMyFollower(userId); if (didAccept) { showSuccessMessage('Follow request accepted'); } ``` ```dart Flutter final status = await AmityCoreClient.newUserRepository() .relationship() .acceptMyFollower(targetUserId); final statusValue = status.value; ``` ## Decline Follow Request Decline an incoming follow request when the current user does not want to approve the requester. ```swift iOS let relationship = AmityUserRelationship() let response = try await relationship.declineMyFollower(withUserId: "requester-user-id") _ = response ``` ```kotlin Android val disposable = AmityCoreClient.newUserRepository() .relationship() .declineMyFollower(userId = targetUserId) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; const didDecline = await UserRepository.Relationship.declineMyFollower(userId); if (didDecline) { showSuccessMessage('Follow request declined'); } ``` ```dart Flutter final status = await AmityCoreClient.newUserRepository() .relationship() .declineMyFollower(targetUserId); final statusValue = status.value; ``` ## Related Topics Create or remove follow relationships. Read status and counters. Query pending or accepted followers. --- ### [Get Follower/Following List](https://learn.social.plus/social-plus-sdk/social/user-relationship/following/get-follower-following-list) > Query paginated follower and following lists with the social.plus SDKs. Use follower and following list APIs when you need to show people lists, pending requests, or accepted relationships. Results are paginated or live depending on the platform. | List | Description | | --- | --- | | Followers | Users who follow the target user | | Following | Users the target user follows | Status filters use `accepted`, `pending`, or `all` where the platform exposes them. TypeScript and Flutter support status filters on target-user follower/following builders. iOS and Android support status filters on the current user's follower/following builders; use their target-user builders when you need another user's accepted social graph without a status filter. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Get followers | `userId` / `targetUserId` | Platform-dependent | Target user whose followers should be queried. iOS and Android status-filter snippets use current-user builders. | | Get following | `userId` / `targetUserId` | Platform-dependent | Target user whose following list should be queried. iOS status-filter snippet uses the current-user builder. | | Both lists | `status` / status filter | No | Relationship status filter such as accepted, pending, or all where exposed. | | Both lists | `limit` / page size | No | Page size for paginated follower or following lists. | ## Get Followers Query followers when your UI needs a paginated people list for a user. The iOS and Android snippets use the current-user builders because those SDKs expose status filters there. ```swift iOS let relationship = AmityUserRelationship() let followers = relationship.getMyFollowers(with: .accepted) _ = followers.observe { collection, error in if let error { handleError(error) return } let relationships = collection.snapshots _ = relationships.first?.sourceUser } followers.nextPage() ``` ```kotlin Android val disposable = AmityCoreClient.newUserRepository() .relationship() .getMyFollowers() .status(AmityFollowStatusFilter.ACCEPTED) .build() .query() .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; let loadMoreFollowers: (() => void) | undefined; const unsubscribe = UserRepository.Relationship.getFollowers( { userId, status: 'accepted' }, ({ data: followers, onNextPage, hasNextPage, loading, error }) => { if (error) { handleError(error); return; } if (!loading && followers) { renderResults(followers); } loadMoreFollowers = hasNextPage ? onNextPage : undefined; }, ); ``` ```dart Flutter final page = await AmityCoreClient.newUserRepository() .relationship() .getFollowers(targetUserId) .status(AmityFollowStatusFilter.ACCEPTED) .getPagingData(limit: 20); final relationships = page.data; ``` ## Get Following Query following relationships when your UI needs the users a profile follows. The iOS snippet uses the current-user builder to show status filtering; the Android snippet uses the target-user builder for another user's following list. ```swift iOS let relationship = AmityUserRelationship() let followings = relationship.getMyFollowings(with: .accepted) _ = followings.observe { collection, error in if let error { handleError(error) return } let relationships = collection.snapshots _ = relationships.first?.targetUser } followings.nextPage() ``` ```kotlin Android val disposable = AmityCoreClient.newUserRepository() .relationship() .getFollowings(userId = targetUserId) .build() .query() .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; let loadMoreFollowings: (() => void) | undefined; const unsubscribe = UserRepository.Relationship.getFollowings( { userId, status: 'accepted' }, ({ data: followings, onNextPage, hasNextPage, loading, error }) => { if (error) { handleError(error); return; } if (!loading && followings) { renderResults(followings); } loadMoreFollowings = hasNextPage ? onNextPage : undefined; }, ); ``` ```dart Flutter final page = await AmityCoreClient.newUserRepository() .relationship() .getFollowings(targetUserId) .status(AmityFollowStatusFilter.ACCEPTED) .getPagingData(limit: 20); final relationships = page.data; ``` ## Follow Relationship Fields | Platform | Common fields | | --- | --- | | TypeScript | `from`, `to`, `status`, `createdAt`, `updatedAt` | | iOS | `sourceUserId`, `targetUserId`, `sourceUser`, `targetUser`, `status` | | Android | `getSourceUser()`, `getTargetUser()`, `getStatus()` | | Flutter | `sourceUserId`, `targetUserId`, `sourceUser`, `targetUser`, `status`, `createdAt` | ## Related Topics Change a relationship. Read counts and status. Process pending followers. --- ### [Get Connection Status](https://learn.social.plus/social-plus-sdk/social/user-relationship/following/get-connection-status) > Read follow status and follow counts for the current user or another user. Use follow info APIs to render profile headers, follow buttons, follower counts, following counts, and pending-request badges. The current user's follow info returns counts only. Another user's follow info returns counts plus the current user's relationship status toward that user. | Field | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Follower count | `followerCount` | `followersCount` | `getFollowerCount()` | `followerCount` | | Following count | `followingCount` | `followingCount` | `getFollowingCount()` | `followingCount` | | Pending count for current user | `pendingCount` | `pendingCount` | `getPendingRequestCount()` | `pendingRequestCount` | | Status for another user | `status` | `status` | `getStatus()` | `status` | ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Get my follow info | None | No | Reads the current user's follow counts and pending-request count. | | Get another user's follow info | `userId` / `targetUserId` | Yes | ID of the user whose counts and relationship status should be read. | | Live observation | Callback / observer / subscription | Platform-dependent | TypeScript, iOS, and Android examples observe live data. Retain and dispose the returned handle where applicable. | ## Get My Follow Info Read the current user's follow counts and pending-request count for profile or account surfaces. ```swift iOS let relationship = AmityUserRelationship() _ = relationship.getMyFollowInfo().observe { liveObject, error in if let error { handleError(error) return } if let info = liveObject.snapshot { _ = info.followersCount _ = info.followingCount _ = info.pendingCount } } ``` ```kotlin Android val disposable = AmityCoreClient.newUserRepository() .relationship() .getMyFollowInfo() .subscribe( { info: AmityMyFollowInfo -> showSuccessMessage(info.getFollowerCount()) }, { error -> handleGeneralError(error) }, ) ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; const unsubscribe = UserRepository.Relationship.getMyFollowInfo( ({ data: followInfo, loading, error }) => { if (error) { handleError(error); return; } if (!loading && followInfo) { updateUI({ followers: followInfo.followerCount, following: followInfo.followingCount, pending: followInfo.pendingCount, }); } }, ); ``` ```dart Flutter final info = await AmityCoreClient.newUserRepository() .relationship() .getMyFollowInfo(); final followerCount = info.followerCount; final followingCount = info.followingCount; final pendingCount = info.pendingRequestCount; ``` ## Get Another User's Follow Info Read another user's follow info when rendering a profile header or follow button state. ```swift iOS let relationship = AmityUserRelationship() _ = relationship.getFollowInfo(withUserId: "target-user-id").observe { liveObject, error in if let error { handleError(error) return } if let info = liveObject.snapshot { _ = info.status _ = info.followersCount _ = info.followingCount } } ``` ```kotlin Android val disposable = AmityCoreClient.newUserRepository() .relationship() .getFollowInfo(userId = targetUserId) .subscribe( { info: AmityUserFollowInfo -> showSuccessMessage(info.getStatus().apiKey) }, { error -> handleGeneralError(error) }, ) ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; const unsubscribe = UserRepository.Relationship.getFollowInfo( userId, ({ data: followInfo, loading, error }) => { if (error) { handleError(error); return; } if (!loading && followInfo) { updateUI({ status: followInfo.status, followers: followInfo.followerCount, following: followInfo.followingCount, }); } }, ); ``` ```dart Flutter final info = await AmityCoreClient.newUserRepository() .relationship() .getFollowInfo(targetUserId); final status = info.status; final followerCount = info.followerCount; final followingCount = info.followingCount; ``` ## Related Topics Change the relationship state. Process pending requests. Query follower and following lists. --- ### [Block & Unblock User](https://learn.social.plus/social-plus-sdk/social/user-relationship/blocking/block-unblock-user) > Block or unblock another user with the social.plus SDKs. Use the block API when the current user wants to block another user. Use the unblock API to remove that block. The SDK updates relationship data after block and unblock calls. TypeScript returns the updated blocked payload (`follows` and `followCounts`). iOS, Android, and Flutter expose these actions as async or completable operations. Blocking affects relationship state, but downstream visibility and interaction behavior can vary by feature area and backend configuration. Keep feed, comment, search, and profile screens resilient by handling errors from those feature APIs and refreshing relationship status after block or unblock actions. ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Block user | `userId` / `targetUserId` | Yes | ID of the user the current user wants to block. | | Unblock user | `userId` / `targetUserId` | Yes | ID of the user the current user wants to unblock. | ## Block User Block a user when the current user wants to prevent or limit relationship-based interactions with that account. ```swift iOS let relationship = AmityUserRelationship() try await relationship.blockUser(userId: "target-user-id") ``` ```kotlin Android val disposable = AmityCoreClient.newUserRepository() .relationship() .blockUser(userId = targetUserId) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; const result = await UserRepository.Relationship.blockUser(userId); const followStatus = result.follows[0]?.status; const followCount = result.followCounts[0]; ``` ```dart Flutter await AmityCoreClient.newUserRepository() .relationship() .blockUser(targetUserId); ``` ### Handle the blocked-user limit A network caps how many users one account can block. When the current user is already at that cap, the block call fails with a "maximum blocked users reached" error instead of succeeding. On TypeScript this surfaces as the server error code `Amity.ServerError.MAX_BLOCKED_USERS_REACHED` (`400324`). Catch it and prompt the user to unblock someone before blocking another account. ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; try { await UserRepository.Relationship.blockUser(userId); } catch (error) { if (error?.code === Amity.ServerError.MAX_BLOCKED_USERS_REACHED) { showBlockedUserLimitReached(); return; } handleError(error); } ``` ```swift iOS let relationship = AmityUserRelationship() do { try await relationship.blockUser(userId: "target-user-id") showSuccessMessage() } catch { if error.isAmityErrorCode(.maxBlockedUsersReached) { // Already at the per-network block cap — prompt the user to unblock someone first. showBlockedUserLimitReached() } else { handleError(error) } } ``` ```kotlin Android AmityCoreClient.newUserRepository() .relationship() .blockUser(userId = targetUserId) .doOnComplete { showSuccessMessage() } .doOnError { error -> if (AmityError.from(error) == AmityError.MAX_BLOCKED_USERS_REACHED) { // Already at the per-network block cap — prompt the user to unblock someone first. showBlockedUserLimitReached() } else { handleGeneralError(error) } } .subscribe() ``` ## Unblock User Unblock a user when the current user removes a previously created block. ```swift iOS let relationship = AmityUserRelationship() try await relationship.unblockUser(userId: "target-user-id") ``` ```kotlin Android val disposable = AmityCoreClient.newUserRepository() .relationship() .unblockUser(userId = targetUserId) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; const result = await UserRepository.Relationship.unBlockUser(userId); const followStatus = result.follows[0]?.status; const followCount = result.followCounts[0]; ``` ```dart Flutter await AmityCoreClient.newUserRepository() .relationship() .unblockUser(targetUserId); ``` ## After Block Or Unblock After a successful block or unblock action: - Refresh follow info if the current screen shows relationship status or counters. - Refresh follower/following lists if the current screen displays social graph data. - Refresh blocked-user lists if the current screen lets users manage blocked accounts. - Handle errors from other feature APIs instead of assuming all product surfaces update in the same way. ## Related Topics Query the blocked-user list. Query the reverse direction. Change follow relationships. --- ### [Manage Blocked Users](https://learn.social.plus/social-plus-sdk/social/user-relationship/blocking/manage-blocked-users) > Query the current user's blocked-user list with the social.plus SDKs. Use blocked-user list APIs to build a settings screen where users can review accounts they have blocked and unblock them when needed. There are two read patterns: | API | Best for | Platforms | | --- | --- | --- | | `getBlockedUsers()` | A paginated management screen | TypeScript, iOS, Android, Flutter | | `getAllBlockedUsers()` | A one-shot list for local decisions | TypeScript, iOS, Android | These APIs return users the current user has **blocked**. To read the reverse direction — users who have **blocked the current user** — see [Users Who Blocked You](./manage-blocking-users). ## Parameters | Operation | Parameter | Required | Description | | --- | --- | --- | --- | | Get blocked users | `limit` / page size | No | Page size for the paginated blocked-user list where the platform exposes it. | | Get blocked users | Pagination handle | No | Use each platform's live collection, paging, or callback pagination to load more blocked users. | | Get all blocked users | None | No | Returns a one-shot list on TypeScript, iOS, and Android. Not exposed in the current public Flutter repository. | ## Get Blocked Users Query blocked users for a paginated account-management screen. ```swift iOS let repository = AmityUserRepository() let blockedUsers = repository.getBlockedUsers() _ = blockedUsers.observe { collection, error in if let error { handleError(error) return } let users = collection.snapshots _ = users.first?.userId } blockedUsers.nextPage() ``` ```kotlin Android val disposable = AmityCoreClient.newUserRepository() .getBlockedUsers() .subscribe( { pagingData: PagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; let loadMoreBlockedUsers: (() => void) | undefined; const unsubscribe = UserRepository.getBlockedUsers( { limit: 25 }, ({ data: users, onNextPage, hasNextPage, loading, error }) => { if (error) { handleError(error); return; } if (!loading && users) { renderResults(users); } loadMoreBlockedUsers = hasNextPage ? onNextPage : undefined; }, ); ``` ```dart Flutter final page = await AmityCoreClient.newUserRepository() .getBlockedUsers() .getPagingData(limit: 20); final blockedUsers = page.data; ``` ## Get All Blocked Users Use `getAllBlockedUsers()` when your app needs a one-shot list instead of a paginated collection. TypeScript, iOS, and Android return up to 100 blocked users and use a short SDK-side cache. Call the method again when you need a fresh snapshot. ```swift iOS let repository = AmityUserRepository() let blockedUsers = try await repository.getAllBlockedUsers() let blockedUserIds = Set(blockedUsers.map { $0.userId }) ``` ```kotlin Android val disposable = AmityCoreClient.newUserRepository() .getAllBlockedUsers() .subscribe( { users: List -> val blockedUserIds = users.map { it.getUserId() }.toSet() showSuccessMessage(blockedUserIds) }, { error -> handleGeneralError(error) }, ) ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; const blockedUsers = await UserRepository.getAllBlockedUsers(); const blockedUserIds = new Set(blockedUsers.map((user) => user.userId)); ``` The one-shot API is not available in the Flutter public repository. For Flutter, use `getBlockedUsers().getPagingData(...)` and page through the result. ## Related Topics Query the reverse direction. Change blocked status. Query social graph lists. ## UIKit — Overview & Getting Started ### [social.plus UIKit](https://learn.social.plus/uikit/overview) > Build beautiful social applications with pre-built UI components. Cross-platform support for iOS, Android, Web, React Native, and Flutter. ![UI Kit Dark Pn](/images/UIKit-Dark.png) # Build Social Apps 10x Faster social.plus UIKit is the **most comprehensive collection of pre-built social UI components** designed to help you create engaging social applications in days, not months. With cross-platform support and extensive customization options, UIKit provides everything you need to build modern social experiences.
Get Started Free Browse Components