# 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 unsubscribe = UserRepository.searchUsers( { displayName: 'John' }, ({ data: users }) => { // users contains 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. The quota is shared across all read endpoints — feed, events, communities, user profiles, and so on — and its size is a network-level setting. 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 social.plus automatically removes the data of inactive guest users, so transient visitor records don't accumulate. This runs on the backend with no integrator action required, and it supports privacy compliance (such as GDPR). Actively-returning visitors keep their data, and a visitor who signs up (converts) keeps 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`, `PIN_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 | An external image URL used as the user's avatar instead of an uploaded `avatarFileId`. | | `metadata` | No | TypeScript, iOS, Android, Flutter | Custom social metadata. Do not store sensitive personal data here. | Once `avatarCustomUrl` is set, the **UIKit renders it automatically** wherever a user's avatar appears — no extra wiring is needed. This is supported on Android UIKit 4.19.2+ and iOS UIKit 4.22.2+. When both `avatarFileId` and `avatarCustomUrl` are set, the custom URL takes precedence. ## 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. ### Selecting which fields to match (`searchBy`) By default the keyword is matched against **all** searchable user fields (display name, user id, and profile handle), so a keyword can surface users that match only by their user id. Provide `searchBy` to restrict which fields are matched — for example, display name only — so results exclude user-id-only matches. - Omitting `searchBy` (or passing an empty value) keeps the default behavior, so existing integrations are unaffected. - Supported field options: `displayName`, `userId`, `profileHandle`. | Field | Platforms | TypeScript | iOS | Android | | --- | --- | --- | --- | --- | | Display name | TypeScript, iOS, Android | `SearchUsersByEnum.DISPLAY_NAME` | `.displayName` | `AmitySearchUserBy.DISPLAY_NAME` | | User ID | TypeScript, iOS, Android | `SearchUsersByEnum.USER_ID` | `.userId` | `AmitySearchUserBy.USER_ID` | | Profile handle | TypeScript, iOS, Android | `SearchUsersByEnum.PROFILE_HANDLE` | `.profileHandle` | `AmitySearchUserBy.PROFILE_HANDLE` | TypeScript types `searchBy` as an array of `SearchUsersByEnum` values, exported from the package root. Plain string literals do not type-check. Platform support for `searchBy` differs. iOS, Android, and TypeScript expose it on user search; Flutter support is not yet available. ### Inputs | Platform | Method | Required inputs | Optional inputs | Result shape | | --- | --- | --- | --- | --- | | TypeScript | `UserRepository.searchUsers(params, callback)` | `displayName` (the keyword), callback | `searchBy`, `limit`, `matchType` | Starts a live collection observer and returns an unsubscriber. | | iOS | `userRepository.searchUsers(keyword, searchBy:, sortBy:, matchType:)` | `keyword`, `searchBy`, `sortBy` | `matchType` | Returns a live collection observed with an `AmityNotificationToken`. | | Android | `userRepository.searchUsers(keyword).build().query()` | `keyword` | `searchBy(...)`, `sortBy(...)`, `matchType(...)` | Returns `Flowable>`. | | Flutter | `AmityCoreClient.newUserRepository().searchUserByDisplayName(keyword)` | `keyword` | `sortBy(...)`, `matchType(...)`, paging token, limit | Returns paging data through the query builder. | TypeScript `UserRepository.searchUserByDisplayName(params, callback)` is deprecated in favour of `searchUsers(params, callback)`. Both accept the same parameters and return the same live collection, so switching is a rename. The deprecated method will be removed in a future major version. 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 { SearchUsersByEnum, UserRepository } from '@amityco/ts-sdk'; let loadMoreUsers: (() => void) | undefined; const unsubscribe = UserRepository.searchUsers( // `searchBy` is optional; omit it to match all fields. { displayName: 'Joe', limit: 20, searchBy: [SearchUsersByEnum.DISPLAY_NAME] }, ({ 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() { // `searchBy` selects which fields the keyword is matched against. // Use [.displayName] to match display name only; pass [] to keep the // default behavior (match all searchable fields). let liveCollection = userRepository.searchUsers( "", searchBy: [.displayName], 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") .searchBy(listOf(AmitySearchUserBy.DISPLAY_NAME)) // optional; omit to match all fields .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 - `searchBy` (where supported) restricts which fields the keyword matches; omit it to keep the default behavior of matching all searchable fields (display name, user id, profile handle). Flutter does not yet expose `searchBy`. - TypeScript `searchUsers(...)` starts a live collection observer, but its search params do not expose `sortBy`. `searchUserByDisplayName(...)` is a deprecated alias with the same behaviour. - 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. - Android takes `searchBy` as a list — `searchBy(listOf(AmitySearchUserBy.DISPLAY_NAME))`. Import the enum from `com.amity.socialcloud.sdk.model.core.search.AmitySearchUserBy`; it lives in a different package from `AmityUserSortOption` and `AmityUserSearchMatchType` (`com.amity.socialcloud.sdk.api.core.user.search`). - 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 with an optional report type, withdraw one report or all of them, and read back which reports you filed, 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. A member can hold several reports on the same user, one per report type plus one typeless report. You can send a simple flag (just the `userId`) or a **structured report** that says _what_ is being reported and _why_, withdraw **one** report or all of them, and read back **which** reports the current member has filed so your UI can disable the options already used. Flag and unflag take a `userId` on TypeScript, iOS, and Android; Flutter takes an `AmityUser` object. Flag state is read from the loaded user object on every platform. Per-report-type unflag and the detailed flag-state result ship in the October 2026 SDK releases for TypeScript, iOS, and Android. Flutter keeps the boolean flag state and the all-at-once unflag. ## Parameters | Operation | Required inputs | Platforms | Result shape | | --------------------- | ---------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | Flag user (typeless) | `userId` | TypeScript, iOS, Android | TypeScript returns `Promise`; iOS async call throws on failure; Android returns `Completable`. | | Report a profile field | `userId`, `field` (optional: `comment`) | TypeScript, iOS, Android | Same as flag user. | | Report behavior | `userId`, `reason` (optional: `comment`) | TypeScript, iOS, Android | Same as flag user. | | Flag user | `AmityUser` object | Flutter | Returns `Future`. | | Unflag user | `userId` (optional: `reportType`) | TypeScript, iOS, Android | TypeScript returns `Promise`; iOS async call throws on failure; Android returns `Completable`. | | Unflag user | `AmityUser` object | Flutter | Returns `Future`. Withdraws all of your reports. | | Quick yes/no | user object | TypeScript, iOS, Android, Flutter | `isFlaggedByMe` on the loaded user object: a boolean hint. | | Check flag status | user object | TypeScript, iOS, Android | A flag-state result from `getUserFlagsByMe()` on the user: `isFlagByMe` plus the list of your own reports (`flags`). TypeScript `Promise`, iOS `AmityUserFlagByMe`, Android `Single`. | | Check flag status | `userId` | TypeScript, iOS | **Deprecated** repository call that still returns a boolean. TypeScript `Promise`; iOS `Bool`. | ## Flag a user Call the flag API when the current user reports another user without saying what or why. The server stores it as a typeless report. ### Inputs | Platform | Method | Required inputs | Result shape | | ---------- | ---------------------------------------- | ------------------ | ------------------------------------------------- | | Android | `userRepository.flagUser(userId)` | `userId` | Returns `Completable`. | | iOS | `userRepository.flagUser(withId:)` | `userId` | Async call that throws on failure. | | TypeScript | `UserRepository.flagUser(userId)` | `userId` | Returns `Promise` — the flagged user. | | Flutter | `user.report().flag()` | `AmityUser` object | Returns `Future`. | ```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() } ``` ```swift iOS func flagUser() async { do { try await userRepository.flagUser(withId: "") print("User flagged") } catch { print("Flag failed: \(error)") } } ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; async function flagUser(userId: string) { const flagged = await UserRepository.flagUser(userId); console.log('Flagged:', flagged); } ``` ```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}'); } } ``` ## Report a profile field or behavior When you want moderators to know _what_ is being reported and _why_, use one of two calls on TypeScript, iOS, and Android: - **Report a profile field** with the field and an optional comment. A profile report never carries a reason. - **Report behavior** with a required reason and an optional comment. | Input | Applies to | Description | | --------- | ---------------- | --------------------------------------------------------------------------------------------------- | | `field` | Profile report | The profile field being reported: display name, description (bio), or avatar. | | `reason` | Behavior report | **Required.** A predefined reason from the shared content report-reason list. | | `comment` | Both | Optional free text from the reporter, up to **300 characters** after trimming. A blank comment is not sent. | ### Inputs | Platform | Method | Required inputs | Result shape | | ---------- | --------------------------------------------------------------- | --------------------------------------- | ---------------------------------- | | Android | `userRepository.flagUserProfile(userId, field, comment?)` | `userId`, `field` (`AmityUserProfileField`) | Returns `Completable`. | | Android | `userRepository.flagUserBehavior(userId, reason, comment?)` | `userId`, `reason` (`AmityContentFlagReasonType`) | Returns `Completable`. | | iOS | `userRepository.flagUserProfile(withId:field:comment:)` | `userId`, `field` (`AmityUserProfileField`) | Async call that throws on failure. | | iOS | `userRepository.flagUserBehavior(withId:reason:comment:)` | `userId`, `reason` (`AmityContentFlagReasonType`) | Async call that throws on failure. | | TypeScript | `UserRepository.flagUserProfile(userId, { field, comment? })` | `userId`, `field` (`UserProfileFieldEnum`) | Returns `Promise`. | | TypeScript | `UserRepository.flagUserBehavior(userId, { reason, comment? })` | `userId`, `reason` (`AmityContentFlagReasonType`) | Returns `Promise`. | ### Profile fields Your app decides the label to show the member. | Field | Suggested label | Android `AmityUserProfileField` | iOS `AmityUserProfileField` | TypeScript `UserProfileFieldEnum` | | -------------- | --------------- | ------------------------------- | --------------------------- | --------------------------------- | | Display name | Username | `DISPLAY_NAME` | `.displayName` | `DISPLAY_NAME` | | Description | Bio | `DESCRIPTION` | `.description` | `DESCRIPTION` | | Avatar | Avatar | `AVATAR` | `.avatar` | `AVATAR` | ### Reasons A behavior report takes a reason from the shared content report-reason list, the same one used when flagging posts, comments, and messages: community guidelines, child endangerment, harassment or bullying, self-harm or suicide, violence or threatening content, selling restricted items, sexual content or nudity, spam or scams, false information, or others. See [Flag reasons](/social-plus-sdk/social/content-management/moderation/content-flagging#flag-reasons) for each platform's identifiers. The reason never carries text: for **Others**, put the reporter's explanation in `comment`. Use **child endangerment** (`CHILD_ENDANGERMENT` on Android, `.childEndangerment` on iOS, `ChildEndangerment` on TypeScript) when a user's behavior endangers a child, including sharing suspected child sexual abuse material (CSAM). ### Rules - A member holds **one report per report type** on a target: one per profile field, one for behavior, and one typeless. Reporting the **same** type again updates that report; reporting a **different** type adds a new one. - A `comment` longer than 300 characters after trimming is rejected **before any request** is sent. - Unflagging without a `reportType` removes **all** of your reports on that user. Pass a `reportType` to withdraw just one — see [Unflag a user](#unflag-a-user). - Any other rule the server enforces returns a validation error (HTTP `422`). ### Examples Use the behavior call when the reporter picks a reason, and the profile call when they point at a field. Add a `comment` to either when the reporter gives extra detail. ```kotlin Android // Report a user's behavior, with a reason and a comment userRepository.flagUserBehavior( userId = userId, reason = AmityContentFlagReasonType.SPAM_OR_SCAMS, comment = "Repeatedly posting promo links" ).subscribe() // Report a profile field (no reason) userRepository.flagUserProfile( userId = userId, field = AmityUserProfileField.AVATAR ).subscribe() ``` ```swift iOS // Report a user's behavior, with a reason and a comment try await userRepository.flagUserBehavior( withId: "", reason: .spamOrScams, comment: "Repeatedly posting promo links" ) // Report a profile field (no reason) try await userRepository.flagUserProfile(withId: "", field: .avatar) ``` ```typescript TypeScript import { AmityContentFlagReasonType, UserProfileFieldEnum, UserRepository } from '@amityco/ts-sdk'; // Report a user's behavior, with a reason and a comment await UserRepository.flagUserBehavior(userId, { reason: AmityContentFlagReasonType.SpamOrScams, comment: 'Repeatedly posting promo links', }); // Report a profile field (no reason) await UserRepository.flagUserProfile(userId, { field: UserProfileFieldEnum.AVATAR }); ``` To report more than one aspect of the same profile (for example the avatar **and** the behavior), send a separate call for each. They are stored as distinct reports. The earlier single call that took `reportType`, `reason`, and `comment` together (`flagUser(userId, options)` on TypeScript, `flagUser(userId, reportType, reason, comment)` on Android, and `flagUser(withId:reportType:reason:comment:)` on iOS) still works but is **deprecated**. Flagging with only a `userId` is not deprecated. ## Unflag a user Call the unflag API when the current user withdraws a report from another user. Without a `reportType` it withdraws every report you filed on that user. With a `reportType` it withdraws only that one and leaves your other reports in place. ### Inputs | Platform | Method | Required inputs | Result shape | | ---------- | ------------------------------------------------ | ------------------------------------- | ---------------------------------- | | Android | `userRepository.unflagUser(userId, reportType?)` | `userId` (optional `reportType`) | Returns `Completable`. | | iOS | `userRepository.unflagUser(withId:reportType:)` | `userId` (optional `reportType`) | Async call that throws on failure. | | TypeScript | `UserRepository.unflagUser(userId, options?)` | `userId` (optional `reportType`) | Returns `Promise`. | | Flutter | `user.report().unflag()` | `AmityUser` object | Returns `Future`. Withdraws all of your reports. | ### Which report is withdrawn | `reportType` | Withdraws | | ---------------------------------------------------------- | ----------------------------------------------- | | omitted | every report you filed on that user | | `displayName`, `description`, `avatarFileId`, `behavior` | only your report of that type | | `untyped` | only your typeless report (the one filed without a `reportType`) | `untyped` exists only for unflag. It names the typeless report, which the flag API never accepts as a type and which the flag-state result returns as `reportType: null`. - Withdrawing a report you never filed succeeds and changes nothing, so withdrawing twice is safe. - Other members' reports on the same user are never affected, even of the same type. - After a per-type unflag you may still hold other reports on that user. Do not set a local "flagged by me" value to `false` after an unflag; read the flag state again instead. ```kotlin Android fun unflagUser(userRepository: AmityUserRepository, userId: String) { // Withdraw every report you filed on this user userRepository.unflagUser(userId = userId) .doOnComplete { Log.d("UserFlag", "User unflagged") } .doOnError { error -> Log.e("UserFlag", "Unflag failed", error) } .subscribe() } fun withdrawBehaviorReport(userRepository: AmityUserRepository, userId: String) { // Withdraw only your behavior report; your other reports stay userRepository.unflagUser( userId = userId, reportType = AmityUserReportTypeFilter.BEHAVIOR ).subscribe() } fun withdrawTypelessReport(userRepository: AmityUserRepository, userId: String) { userRepository.unflagUser( userId = userId, reportType = AmityUserReportTypeFilter.UNTYPED ).subscribe() } ``` ```swift iOS func unflagUser() async { do { // Withdraw every report you filed on this user try await userRepository.unflagUser(withId: "") print("User unflagged") } catch { print("Unflag failed: \(error)") } } func withdrawBehaviorReport() async throws { // Withdraw only your behavior report; your other reports stay try await userRepository.unflagUser(withId: "", reportType: .behavior) } func withdrawTypelessReport() async throws { try await userRepository.unflagUser(withId: "", reportType: .untyped) } ``` ```typescript TypeScript import { UserReportTypeFilterEnum, UserRepository } from '@amityco/ts-sdk'; // Withdraw every report you filed on this user async function unflagUser(userId: string) { const unflagged = await UserRepository.unflagUser(userId); console.log('Unflagged:', unflagged); } // Withdraw only your behavior report; your other reports stay async function withdrawBehaviorReport(userId: string) { await UserRepository.unflagUser(userId, { reportType: UserReportTypeFilterEnum.BEHAVIOR }); } // Withdraw only your typeless report async function withdrawTypelessReport(userId: string) { await UserRepository.unflagUser(userId, { reportType: UserReportTypeFilterEnum.UNTYPED }); } ``` ```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 which reports you filed on a user Use the flag-state API to decide what to show: a "reported" badge, which report options to disable because the current member already used them, and a withdraw action per report. Only the current member's own reports come back; other members' reports are never exposed. The repository call `isUserFlaggedByMe` is **deprecated** on TypeScript and iOS. It still works and still returns a boolean, so nothing breaks. Read flag state from the user object instead: `isFlaggedByMe` for a quick yes/no and `getUserFlagsByMe()` for the reports. Android never had a repository call; it adds `getUserFlagsByMe()` next to its existing `isFlaggedByMe()`. ### Which one to use There are two ways to read the flag state. Pick by what the screen is doing. | When | Use | What you get | | ---- | --- | ------------ | | **Drawing a feed row, a list item or a "reported" badge** | The quick yes/no on the user object: Android `user.isFlaggedByMe()`, iOS `user.isFlaggedByMe`, TypeScript `user.isFlaggedByMe`, Flutter `user.isFlaggedByMe` | A boolean that **returns immediately** and never waits for a request. It is computed from the user data when the user is loaded. | | **Showing the options to flag or unflag** | The flag-state call on the user object: Android, iOS, and TypeScript `user.getUserFlagsByMe()` | The exact reports (`isFlagByMe` and `flags`), read from the server. Call it **only when you open those options**, not for every row. When the quick yes/no is `false` it returns no reports without a request. | The quick yes/no is a hint built from data the user object already carries: - `false` is exact: you have not reported this user, as of the user object's last update. - `true` means "maybe". It can be `true` for a user you did not report, because of other members' reports. - On TypeScript, iOS and Android, the SDK checks a `true` with the server in the background, once each time the user's reports change, and remembers the answer. On TypeScript and iOS an observed user is delivered again with the checked value. On Android the checked value appears the next time the user updates. #### Keep the quick yes/no current A loaded user object is a snapshot: the yes/no is worked out from the user data it was built from, so a user object you already hold does **not** change after you flag or unflag. Observe the user instead, and read `isFlaggedByMe` from each update. The observer delivers a new user object after every flag or unflag, so your row or badge stays current without any request. ```kotlin Android fun observeFlagState(userRepository: AmityUserRepository, userId: String): Disposable { // A new AmityUser arrives after you flag or unflag, so isFlaggedByMe() stays current return userRepository.getUser(userId = userId) .doOnNext { user: AmityUser -> val showReported = user.isFlaggedByMe() // hint, no request // Update your row or badge here } .subscribe() } // Call dispose() on the returned Disposable when the screen no longer needs user updates. ``` ```swift iOS var token: AmityNotificationToken? func observeFlagState(userId: String) { // A new AmityUser arrives after you flag or unflag, so isFlaggedByMe stays current token = userRepository.getUser(userId).observe { liveObject, error in guard let user = liveObject.snapshot else { return } let showReported = user.isFlaggedByMe // hint, no request // Update your row or badge here } } ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; // A new Amity.User arrives after you flag or unflag, so isFlaggedByMe stays current const unsubscribe = UserRepository.getUser(userId, ({ data: user, loading }) => { if (loading || !user) return; const showReported = user.isFlaggedByMe; // hint, no request // Update your row or badge here }); // Call unsubscribe() when the screen no longer needs user updates. ``` A report filed on another device shows once the user is refreshed. Call `getUserFlagsByMe()` when you need the exact state. `getUserFlagsByMe()` uses the hint of the user object you call it on: when that hint is `false` (including a `false` the SDK has already confirmed) it returns no reports without a request. Call it on the user you just observed, or on a freshly loaded one, not on an object you held from before a flag or unflag. #### Load the user, then check `getUserFlagsByMe()` is called on a user object, so get the user first. Use this when the options to flag or unflag open. A user you already hold from a list, a profile, or a member row works too; it carries the same members. ```kotlin Android fun openReportOptions(userRepository: AmityUserRepository, userId: String) { // 1. Get the user. firstOrError() takes the first value and stops. userRepository.getUser(userId = userId) .firstOrError() // 2. Read the exact reports now .flatMap { user: AmityUser -> user.getUserFlagsByMe() } .doOnSuccess { result -> val isReported = result.isFlagByMe // Show "Unreport" when isReported is true, otherwise "Report" } .subscribe() } ``` ```swift iOS var token: AmityNotificationToken? func openReportOptions(userId: String) { // 1. Get the user. observeOnce delivers the first value and stops. token = userRepository.getUser(userId).observeOnce { liveObject, error in guard let user = liveObject.snapshot else { return } Task { // 2. Read the exact reports now let result = try? await user.getUserFlagsByMe() let isReported = result?.isFlagByMe ?? false // Show "Unreport" when isReported is true, otherwise "Report" } } } ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; async function openReportOptions(user: Amity.User) { // The user comes from getUser, a list, or a member row; read the exact reports now const { isFlagByMe } = await user.getUserFlagsByMe(); // Show "Unreport" when isFlagByMe is true, otherwise "Report" } const unsubscribe = UserRepository.getUser(userId, ({ data: user, loading }) => { if (loading || !user) return; openReportOptions(user); }); ``` #### Migrating from the repository call The repository call still works but is deprecated on TypeScript and iOS. Replace it with the user object: `isFlaggedByMe` where you drew a badge from the boolean, and `getUserFlagsByMe()` where you need the exact answer. ```swift iOS // Before (deprecated, still returns Bool) let isFlagged = try await userRepository.isUserFlaggedByMe(withId: userId) // After userRepository.getUser(userId).observeOnce { liveObject, error in guard let user = liveObject.snapshot else { return } Task { let isFlagged = try await user.getUserFlagsByMe().isFlagByMe } } ``` ```typescript TypeScript import { UserRepository } from '@amityco/ts-sdk'; // Before (deprecated, still returns a boolean) const isFlagged = await UserRepository.isUserFlaggedByMe(userId); // After: a badge needs no request, the options sheet reads the exact state UserRepository.getUser(userId, async ({ data: user, loading }) => { if (loading || !user) return; const showReported = user.isFlaggedByMe; const { isFlagByMe } = await user.getUserFlagsByMe(); }); ``` ### Inputs | Platform | Method | Required inputs | Result shape | | ---------- | --------------------------------------------- | ------------------ | --------------------------------------------------------------- | | Android | `user.isFlaggedByMe()` | `AmityUser` object | Returns `Boolean`, a quick hint with no request. | | Android | `user.getUserFlagsByMe()` | `AmityUser` object | Returns `Single`. | | iOS | `user.isFlaggedByMe` | `AmityUser` object | Returns `Bool`, a quick hint with no request. | | iOS | `user.getUserFlagsByMe()` | `AmityUser` object | Returns `AmityUserFlagByMe` from an async call. | | iOS | `userRepository.isUserFlaggedByMe(withId:)` | `userId` | **Deprecated.** Returns `Bool` from an async call. | | TypeScript | `user.isFlaggedByMe` | `Amity.User` object | Returns `boolean`, a quick hint with no request. | | TypeScript | `user.getUserFlagsByMe()` | `Amity.User` object | Returns `Promise`. | | TypeScript | `UserRepository.isUserFlaggedByMe(userId)` | `userId` | **Deprecated.** Returns `Promise`. | | Flutter | `user.isFlaggedByMe` | `AmityUser` object | Returns `bool` from the loaded user model (yes/no only). | ### Result | Field | Type | Description | | -------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `isFlagByMe` | boolean | `true` when the current member holds any report on the user. | | `flags` | list | The current member's own reports on the user, in server order. Empty when not reported. | | `flags[].reportType` | `displayName`, `description`, `avatarFileId`, `behavior`, or null | Which aspect was reported. `null` is the typeless report; withdraw it with `untyped`. | | `flags[].reason` | string or null | The stored reason. Present on `behavior` reports. | | `flags[].comment` | string or null | The stored reporter comment. | | `flags[].createdAt` | date-time or null | When the report was filed. `null` for reports filed before report types existed. | Every field is always present; a field the report does not carry is `null`. The result is read from the server on each call and is not cached. The one exception: when the user's quick yes/no is `false`, `getUserFlagsByMe()` returns no reports without a request. ```kotlin Android fun checkFlagState(user: AmityUser) { // A hint with no request: false is exact; a true is checked with the server in the background Log.d("UserFlag", "Probably flagged by me: ${user.isFlaggedByMe()}") // The exact reports; skips the request when the hint is false user.getUserFlagsByMe() .doOnSuccess { result -> Log.d("UserFlag", "Flagged by me: ${result.isFlagByMe}") // Disable the report options the member already used val usedTypes = result.flags.map { it.reportType }.toSet() val canReportBio = AmityUserReportType.DESCRIPTION !in usedTypes Log.d("UserFlag", "Can report bio: $canReportBio") } .doOnError { error -> Log.e("UserFlag", "Flag-state check failed", error) } .subscribe() } ``` ```swift iOS func checkFlagState(user: AmityUser) async { // A hint with no request: false is exact; a true is checked with the server in the background print("Probably flagged by me: \(user.isFlaggedByMe)") do { // The exact reports; skips the request when the hint is false let result = try await user.getUserFlagsByMe() print("Flagged by me: \(result.isFlagByMe)") // Disable the report options the member already used let usedTypes = Set(result.flags.map { $0.reportType }) let canReportBio = !usedTypes.contains(.description) print("Can report bio: \(canReportBio)") } catch { print("Flag-state check failed: \(error)") } } ``` ```typescript TypeScript import { UserReportTypeEnum } from '@amityco/ts-sdk'; async function checkFlagState(user: Amity.User) { // A hint with no request: false is exact; a true is checked with the server in the background console.log('Probably flagged by me:', user.isFlaggedByMe); // The exact reports; skips the request when the hint is false const { isFlagByMe, flags } = await user.getUserFlagsByMe(); console.log('Flagged by me:', isFlagByMe); // Disable the report options the member already used; null marks the typeless report const usedTypes = new Set(flags.map(flag => flag.reportType)); const canReportBio = !usedTypes.has(UserReportTypeEnum.DESCRIPTION); console.log('Can report bio:', canReportBio); } ``` ```dart Flutter void checkFlagState(AmityUser user) { final isFlaggedByMe = user.isFlaggedByMe; print('Flagged by me: $isFlaggedByMe'); } ``` ### Withdraw one of the listed reports Each entry in `flags` maps to one unflag call. Pass its `reportType`, or `untyped` when the entry's `reportType` is `null`. On Android and iOS the `reportType` parameter of unflag is optional, and omitting it (or passing `nil`) withdraws **all** of your reports. An entry whose `reportType` is `null` is the typeless report: pass `untyped`, not `nil`. ```kotlin Android fun withdrawAllListedReports(userRepository: AmityUserRepository, user: AmityUser) { user.getUserFlagsByMe() .flatMapCompletable { result -> Completable.concat( result.flags.map { flag -> val filter = when (flag.reportType) { AmityUserReportType.DISPLAY_NAME -> AmityUserReportTypeFilter.DISPLAY_NAME AmityUserReportType.DESCRIPTION -> AmityUserReportTypeFilter.DESCRIPTION AmityUserReportType.AVATAR -> AmityUserReportTypeFilter.AVATAR AmityUserReportType.BEHAVIOR -> AmityUserReportTypeFilter.BEHAVIOR null -> AmityUserReportTypeFilter.UNTYPED } userRepository.unflagUser(userId = user.getUserId(), reportType = filter) } ) } .subscribe() } ``` ```swift iOS func withdrawAllListedReports(user: AmityUser) async throws { let result = try await user.getUserFlagsByMe() for flag in result.flags { let filter: AmityUserReportTypeFilter switch flag.reportType { case .displayName: filter = .displayName case .description: filter = .description case .avatarFileId: filter = .avatarFileId case .behavior: filter = .behavior case .none: filter = .untyped @unknown default: filter = .untyped } try await userRepository.unflagUser(withId: user.userId, reportType: filter) } } ``` ```typescript TypeScript import { UserReportTypeFilterEnum, UserRepository } from '@amityco/ts-sdk'; async function withdrawAllListedReports(user: Amity.User) { const { flags } = await user.getUserFlagsByMe(); for (const flag of flags) { // A null reportType is the typeless report; unflag selects it with 'untyped' const reportType = flag.reportType ?? UserReportTypeFilterEnum.UNTYPED; await UserRepository.unflagUser(user.userId, { reportType }); } } ``` ## 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 and unflag by `userId`, and read flag state from the loaded user object (`isFlaggedByMe` and `getUserFlagsByMe()`). On TypeScript every user the SDK returns carries both members: a user from `getUser`, a user list, a member list, or a message preview. - Flutter flags and unflags through the loaded `AmityUser` object, withdraws all reports at once, and exposes a boolean flag state only. - The child endangerment reason for behavior reports ships in TypeScript 7.31.0 and in the next Android and iOS releases. - The repository call `isUserFlaggedByMe` is deprecated on TypeScript and iOS and keeps returning a boolean: `Promise` on TypeScript, `Bool` on iOS. Android has no repository call. - `isFlaggedByMe` is computed from the user data when the user is loaded. `false` is exact; `true` can be a false positive, and it only reflects the user as of its last update. TypeScript, iOS and Android check a `true` with the server in the background. Use it to draw a feed row or a badge, and call `getUserFlagsByMe()` when you show the options to flag or unflag. `getUserFlagsByMe()` skips the request when `isFlaggedByMe` is `false`. - The backend returns the updated user, including its current `flagCount`, and the SDK saves it. Read flag state after a flag or unflag when your UI needs the updated per-report list. ## 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.plus 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.plus 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.getWidth(), height: image.getHeight(), }; } ``` ```swift iOS let rawFile = try await fileRepository.getFile(fileId: imageFileId) if rawFile.type == .image, let imageData = rawFile.mapToImageData() { let width = imageData.getWidth() let height = imageData.getHeight() 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.plus 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 | Frame dimensions | Notes | | --- | --- | --- | --- | --- | --- | | TypeScript | `FileRepository.uploadVideo(formData, feedType?, onProgress?)` | `FileRepository.getFile(fileId)` | `status` and `videoUrl` on the returned file | `getWidth()`, `getHeight()`, and `getRotation()` on a `'video'` file. Requires `@amityco/ts-sdk` 7.27.0 or later. | `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:)` | No dedicated accessors. Read `attributes["metadata"]["video"]` off `AmityVideoData`. | 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)` | `getWidth()`, `getHeight()`, and `getRotation()` on `AmityVideo`. Requires Android SDK 7.26.0 or later. | `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 | `fileProperties.width` and `fileProperties.height` only. Rotation is not exposed. | `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. The returned video file also carries the frame dimensions, but each platform surfaces them differently — see the **Frame dimensions** column above for the accessor and the minimum SDK version. Width and height are the **stored** (pre-rotation) values. Apply the rotation to get the displayed orientation: swap width and height when `rotation % 180 != 0`. Flutter exposes the stored width and height but **no rotation**. Width and height alone cannot determine the display orientation of a rotated video; use a media player or parser that handles orientation metadata. ```typescript TypeScript import { FileRepository } from '@amityco/ts-sdk'; async function getVideo(videoFileId: string) { // getFile does not narrow its return type, so cast to the video file shape // before reading the video-only members. const { data } = await FileRepository.getFile(videoFileId); const video = data as Amity.File<'video'>; const playbackUrl = video.videoUrl?.['720p'] ?? video.fileUrl; return { playbackUrl, status: video.status, width: video.getWidth(), height: video.getHeight(), rotation: video.getRotation(), }; } ``` ```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) // Stored frame dimensions. iOS has no dedicated accessors — read the raw metadata. let videoStream = (videoData.attributes["metadata"] as? [String: Any])?["video"] as? [String: Any] let width = videoStream?["width"] as? Int let height = videoStream?["height"] as? Int let rotation = videoStream?["rotation"] as? Int } ``` ```kotlin Android import com.amity.socialcloud.sdk.helper.core.asAmityVideo 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) val width = video?.getWidth() val height = video?.getHeight() val rotation = video?.getRotation() } .subscribe() ``` ```dart Flutter void inspectVideo(AmityVideo video) { final resolutions = video.getResolutions(); final playbackUrl = video.getVideoUrl(AmityVideoResolution.RES_720); // Stored frame dimensions. They live on the file properties, not on AmityVideo, // and Flutter does not expose the rotation that goes with them. final int? width = video.fileProperties.width; final int? height = video.fileProperties.height; } ``` ## 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.plus 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.plus 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.plus 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 | Platforms | Description | | --- | --- | --- | --- | | `referenceType` | Yes | TypeScript, iOS, Android, Flutter | Content type being reacted to. Supported values are post, comment, story, and message. | | `referenceId` | Yes | TypeScript, iOS, Android, Flutter | ID of the post, comment, story, or message. | | `reactionName` | Yes for add/remove, optional for query | TypeScript, iOS, Android, Flutter | Reaction key such as `like`. Query APIs can use it as a filter. | | `callback` / live collection observer | Query only | TypeScript, iOS, Android, Flutter | 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.plus 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.plus 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.plus 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 | Platforms | Description | | --- | --- | --- | --- | | `postId` or another entity ID | Live object methods | Android | ID of the object to observe. | | Query builder target | Live collection builders | Android | Scope for the collection, such as `targetCommunity(communityId)` or `targetUser(userId)`. | | `PagingData` | Paged collections | Android | The emitted paging payload consumed by RecyclerView, Paging 3, or Compose paging. | | `subscribeOn(Schedulers.io())` | RxJava streams | Android | Runs SDK work on an IO scheduler. | | `observeOn(AndroidSchedulers.mainThread())` | UI observers | Android | 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.plus 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 | Platforms | Description | | --- | --- | --- | --- | | `postId` or another entity ID | Live object streams | Flutter | ID of the object to observe. | | Query builder target | Live collections | Flutter | Scope for the collection, such as `targetCommunity(communityId)` or `targetUser(userId)`. | | `pageSize` | Live collections | Flutter | Optional page size for `getLiveCollection(pageSize: ...)`. Defaults to 20 when omitted. | | `LiveResult.data` | Collection stream | Flutter | Current list snapshot. | | `LiveResult.isFetching` | Collection stream | Flutter | 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.plus 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.plus 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 | Platforms | Description | | --- | --- | --- | --- | | `postId` or another entity ID | Live object methods | TypeScript | 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()` | TypeScript | Scope for the list. For posts, `targetType` is commonly `user` or `community`. | | `limit` | Collection queries | TypeScript | Optional page size for the query. | | `callback` | Object and collection methods | TypeScript | Receives each live snapshot. Handle `loading` and `error` before rendering data. | | `onNextPage` / `hasNextPage` | Live collections | TypeScript | Use when the UI asks for the next page. Do not call `onNextPage` repeatedly without checking `hasNextPage`. | | `subscribeTopic(topic, callback?)` | Real-time updates | TypeScript | 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, MessageRepository, MessageContentType } from '@amityco/ts-sdk'; // Session-level: a global ban ends the session. const unsubscribe = Client.onConnectionError(error => { if (error.code === Amity.ServerError.GLOBAL_BAN) { console.log('The current user is globally banned.'); } }); unsubscribe(); // Action-level: global mute and unsafe content reject only the failed call. async function sendTextMessage(subChannelId: string, text: string) { try { await MessageRepository.createMessage({ subChannelId, dataType: MessageContentType.TEXT, data: { text }, }); } catch (error) { const { code } = error as { code?: number }; if (code === Amity.ServerError.GLOBAL_MUTE) { console.log('The current user is globally muted and cannot post right now.'); } else if (code === Amity.ServerError.UNSAFE_CONTENT) { console.log('The content was rejected by moderation. Edit it before sending again.'); } else { throw error; } } } ``` ```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.") } } // Action-level: global mute and unsafe content reject only the failed call. fun sendTextMessage(subChannelId: String, text: String): Disposable { return AmityChatClient.newMessageRepository() .createTextMessage(subChannelId, text) .build() .send() .subscribe({}, { exception -> when (AmityError.from(exception)) { AmityError.USER_IS_GLOBAL_MUTED -> print("The current user is globally muted and cannot post right now.") AmityError.UNSAFE_CONTENT -> print("The content was rejected by moderation. Edit it before sending again.") else -> print(exception) } }) } ``` ```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` | | Globally muted user (`400313`) | `Amity.ServerError.GLOBAL_MUTE` | `.globalMute` | `AmityError.USER_IS_GLOBAL_MUTED` | Not exposed as a public Flutter SDK enum in the current source | | Unsafe content rejected (`400314`) | `Amity.ServerError.UNSAFE_CONTENT` | `.unsafeContent` | `AmityError.UNSAFE_CONTENT` | Not exposed as a public Flutter SDK enum in the current source | | 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` | A global mute (`400313`) is not a global ban (`400312`). The user stays signed in and the session continues; only the blocked action fails. Unsafe content (`400314`) means the request was rejected by content moderation, so sending the same content again fails the same way. Neither code ends the session, so handle both as a per-action error: tell the user why the action failed instead of signing them out. ## 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, global-ban, global-mute, or unsafe-content 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 } } ``` ### Thumbnail and Resolution Alongside the playback URL, the room exposes a thumbnail and the stream's dimensions — useful for a poster image before playback starts and for sizing the player without waiting for the first frame. | Field | TypeScript | iOS | Android | Available | | --- | --- | --- | --- | --- | | Live thumbnail | `room.liveThumbnailUrl` | `room.liveThumbnailUrl` | `room.getLiveThumbnailUrl()` | Live or reconnecting sessions, when generated | | Live resolution | `room.liveResolution` | `room.liveResolution` | `room.getLiveResolution()` | Live or reconnecting sessions, when available | | Recorded resolution | `room.recordedResolution` | `room.recordedResolution` | `room.getRecordedResolution()` | After the recording is available | Resolution carries the stream's aspect ratio, width, and height. These fields can be absent early in a session, so treat them as optional and fall back to your own placeholder. See [Rooms Overview](./rooms-overview) for the full room field reference. ## 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. --- ### [Live Viewer Count Config](https://learn.social.plus/social-plus-sdk/video-new/broadcasting/viewer-count-config) > Read the network-level configuration that governs whether viewers see the live viewer count on a livestream. `AmityClient.getLiveViewerCountConfig()` returns the network-level configuration that governs whether the live viewer count is shown to viewers on a livestream. Admins pick one of three modes (always show, hide entirely, or show above a minimum) at the network level; the SDK reads it as a one-shot cold read and the UIKit's viewer-count element applies the rule. This page covers the SDK read API. The write path is admin-only and served by the Console — the SDK does not update this setting. Console UI and permissions are documented separately in [Network Settings](/analytics-and-moderation/social+-apis-and-services/network-settings). ## Platform Surface | Platform | Entry point | Return type | Signature | | --- | --- | --- | --- | | TypeScript | `client.getLiveViewerCountConfig()` | `Promise` | Zero-argument, async | | iOS | `client.getLiveViewerCountConfig()` | `AmityLiveViewerCountConfig` | `async throws` on `AmityClient` | | Android | `AmityCoreClient.getLiveViewerCountConfig()` | `Single` | Zero-argument, RxJava | ## Modes Three modes are available. They apply to viewers only — **hosts and co-hosts always see the actual count regardless of mode**. | Mode | Threshold applied | Viewer behavior | | --- | --- | --- | | `alwaysShow` | No | Count is always visible. Default for never-configured networks. | | `hideEntirely` | No | Viewers never see the count. Hosts and co-hosts still see it. | | `showAboveMinimum` | Yes (inclusive) | Viewers see the count only when `count >= threshold`. | ## Config Model | Field | Type | Description | | --- | --- | --- | | `mode` | `AmityLiveViewerCountMode` | Selected display mode. | | `threshold` | `Int` | Positive integer in `[1, 1000]`. **Always present**, default `50`. Applied only under `showAboveMinimum`. | ## Read the Config Read the config at each livestream mount. The API is a cold read — there is no observable and no real-time channel. Admin changes made mid-stream do not propagate to already-connected viewers; they take effect only on a subsequent mount (leave and rejoin). ```typescript TypeScript import { Client, AmityLiveViewerCountMode } from "@amityco/ts-sdk"; async function shouldShowViewerCount( client: Amity.Client, currentCount: number, isHostOrCoHost: boolean, ): Promise { if (isHostOrCoHost) { return true; } const config = await client.getLiveViewerCountConfig(); switch (config.mode) { case AmityLiveViewerCountMode.ALWAYS_SHOW: return true; case AmityLiveViewerCountMode.HIDE_ENTIRELY: return false; case AmityLiveViewerCountMode.SHOW_ABOVE_MINIMUM: return currentCount >= config.threshold; } } ``` ```kotlin Android import com.amity.socialcloud.sdk.api.core.AmityCoreClient import com.amity.socialcloud.sdk.model.core.settings.AmityLiveViewerCountMode fun observeShouldShowCount( currentCount: Int, isHostOrCoHost: Boolean, ) { if (isHostOrCoHost) { showSuccessMessage(true) return } AmityCoreClient.getLiveViewerCountConfig() .subscribe( { config -> val visible = when (config.mode) { AmityLiveViewerCountMode.ALWAYS_SHOW -> true AmityLiveViewerCountMode.HIDE_ENTIRELY -> false AmityLiveViewerCountMode.SHOW_ABOVE_MINIMUM -> currentCount >= config.threshold } showSuccessMessage(visible) }, { error -> handleGeneralError(error) } ) } ``` ```swift iOS import AmitySDK func shouldShowViewerCount( client: AmityClient, currentCount: Int, isHostOrCoHost: Bool ) async throws -> Bool { if isHostOrCoHost { return true } let config = try await client.getLiveViewerCountConfig() switch config.mode { case .alwaysShow: return true case .hideEntirely: return false case .showAboveMinimum: return currentCount >= config.threshold } } ``` ## Visibility Rule The rule is evaluated in a fixed order — the first match wins. 1. **Role check first.** If the current user is a **host** or **co-host**, show the actual count. All mode-based hiding applies to viewers only. 2. **Mode branch** (viewer role): - `alwaysShow` — show. - `hideEntirely` — hide. - `showAboveMinimum` — show iff `count >= threshold` (inclusive). The comparison is inclusive: at exactly `count == threshold`, the count is shown. ## Propagation Model The SDK reads the config once when the viewer joins a stream. Changes made by an admin while the viewer is already watching **do not** update the visibility state — the viewer keeps the rule that applied at join. To pick up a new config, the viewer must leave and rejoin the stream. Within a single mount, the visibility state still reacts to two things: - **Count ticks.** Under `showAboveMinimum`, if the viewer count crosses the threshold naturally, the count appears or disappears without a rejoin. - **Role changes.** A viewer promoted to co-host mid-stream immediately sees the actual count — no rejoin needed. ## Threshold Constraints | Constraint | Value | | --- | --- | | Type | Positive integer | | Minimum | `1` | | Default | `50` | | Maximum | `1000` | | Comparison | Inclusive (`count >= threshold`) | The Console clamps admin-entered values on save: values above `1000` are clamped to `1000`; values below `1` (or empty) reset to `1`. Zero is explicitly rejected — use `alwaysShow` for "always visible" instead. ## Defaults Never-configured networks read as `{ mode: alwaysShow, threshold: 50 }` — the backend merges defaults at read time, so no client-side fallback is required for absent fields. Every read after an admin change returns the current value; no client-side cache is required. ## Error Handling On any read error or malformed config, **fail open** — behave as `alwaysShow` (the UIKit `LiveViewerCountElement` does this automatically), so today's behavior is never silently regressed. Never treat a config error as a reason to hide the count. If the admin never selected `hideEntirely`, viewers should still see it. Fail open, always. ## UIKit Integration If you use UIKit, no wiring is needed — the [Livestream Player Page](/uikit/components/social/livestream) reads this config automatically through the built-in `LiveViewerCountElement`. Use this API only when you build a custom viewer surface and need to enforce the same visibility rule yourself. ## Related Topics Observe room playback state and hand playback URLs to your player. Manage co-host participants — co-hosts are exempt from the visibility rule. Ready-to-use livestream components that apply the visibility rule automatically. --- ### [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. Pin one message above a livestream chat and keep it in sync through the channel live object. ## 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 chat messages with an optional reason and detail, unflag them, and read the current user's flag state. 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 | Unflag | Check current user's flag | | --- | --- | --- | --- | | Android | `messageRepository.flagMessage(messageId)` or `flagMessage(messageId, reason, detail?)` | `messageRepository.unflagMessage(messageId)` | `message.isFlaggedByMe()` on a fetched `AmityMessage` | | iOS | `messageRepository.flagMessage(withId:)` or `flagMessage(withId:reason:detail:)` | `messageRepository.unflagMessage(withId:)` | `messageRepository.isMessageFlaggedByMe(withId:)` | | TypeScript | `MessageRepository.flagMessage(messageId)` or `flagMessage(messageId, { reason, detail? })` | `MessageRepository.unflagMessage(messageId)` | `MessageRepository.isMessageFlaggedByMe(messageId)` | | Flutter | `messageRepository.flagMessage(messageId:reason:)` or `message.flagWithReason(reason)` | `messageRepository.unflag(messageId)` or `message.unflag()` | `message.isFlaggedByMe` on a fetched `AmityMessage` | ## Parameters | Operation | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | --- | | Flag | `messageId` | Yes | All | Message ID to report. | | Flag | `reason` | No on Android, iOS, TypeScript; yes on Flutter `flagMessage` | All | One reason category. Android, iOS, and TypeScript use `AmityContentFlagReasonType` (`ContentFlagReasonEnum` before TypeScript 7.31.0), Flutter uses `AmityContentFlagReason`. See [Flag reasons](/social-plus-sdk/social/content-management/moderation/content-flagging#flag-reasons). | | Flag | `detail` | No | Android, iOS, TypeScript | The reporter's own words, for any reason including Others. Needs a `reason`. Sent as given; omitted when not set. The server limits it to 300 characters. | | Unflag | `messageId` | Yes | All | Message ID whose report should be removed for the current user. | | Check flag state | `messageId` | Yes | iOS, TypeScript | Message ID to check through the repository. | | Check flag state | Fetched message object | Yes | Android, Flutter | `isFlaggedByMe` on a fetched `AmityMessage`. | ## Flag A Message Flag a message with a reason so your product can route it into review, and add the reporter's own words as `detail` when they give some. Use the no-reason call when your UI only offers a single "Report" action. ```kotlin Android // Reason with the reporter's own words messageRepository .flagMessage( messageId = messageId, reason = AmityContentFlagReasonType.SPAM_OR_SCAMS, detail = "Same promo link sent to every member", ) .subscribe( { showSuccessMessage() }, { error -> handleFlagError(error) }, ) // No reason messageRepository .flagMessage(messageId = messageId) .subscribe( { showSuccessMessage() }, { error -> handleFlagError(error) }, ) ``` ```swift iOS // Reason with the reporter's own words try await messageRepository.flagMessage( withId: "message-id", reason: .spamOrScams, detail: "Same promo link sent to every member" ) // Reason only: type the reason explicitly so the current call is used try await messageRepository.flagMessage( withId: "message-id", reason: AmityContentFlagReasonType.spamOrScams ) // No reason try await messageRepository.flagMessage(withId: "message-id") showSuccessMessage("Message flagged") ``` ```typescript TypeScript import { AmityContentFlagReasonType, MessageRepository } from '@amityco/ts-sdk'; // Reason with the reporter's own words const flagged = await MessageRepository.flagMessage(messageId, { reason: AmityContentFlagReasonType.SpamOrScams, detail: 'Same promo link sent to every member', }); renderResults(flagged); // No reason await MessageRepository.flagMessage(messageId); ``` ```dart Flutter final flaggedMessage = await AmityChatClient.newMessageRepository() .flagMessage( messageId: messageId, reason: AmityContentFlagReason.spamOrScams, ); final isFlaggedByMe = flaggedMessage.isFlaggedByMe; ``` ## Report Child Endangerment Flag a message that endangers a child, including suspected child sexual abuse material (CSAM), with the Child endangerment reason. Send it as its own reason, not as Others with a detail, so these reports reach moderators under their own category. A `detail` is optional. {/* doc-as-test: skip (CHILD_ENDANGERMENT ships after the published Android SDK) */} ```kotlin Android messageRepository .flagMessage( messageId = messageId, reason = AmityContentFlagReasonType.CHILD_ENDANGERMENT, ) .subscribe( { showSuccessMessage() }, { error -> handleFlagError(error) }, ) ``` {/* doc-as-test: skip (childEndangerment ships after the published iOS SDK) */} ```swift iOS // Type the reason explicitly so the current call is used try await messageRepository.flagMessage( withId: "message-id", reason: AmityContentFlagReasonType.childEndangerment ) ``` ```typescript TypeScript import { AmityContentFlagReasonType, MessageRepository } from '@amityco/ts-sdk'; await MessageRepository.flagMessage(messageId, { reason: AmityContentFlagReasonType.ChildEndangerment, }); ``` ## Report With Others Use the Others reason when none of the categories fit. On Android, iOS, and TypeScript, put the reporter's explanation in `detail`, as for any other reason. Flutter carries the text on its Others reason. ```kotlin Android messageRepository .flagMessage( messageId = messageId, reason = AmityContentFlagReasonType.OTHERS, detail = "Contains sensitive account information", ) .subscribe( { showSuccessMessage() }, { error -> handleFlagError(error) }, ) ``` ```swift iOS try await messageRepository.flagMessage( withId: "message-id", reason: .others, detail: "Contains sensitive account information" ) ``` ```typescript TypeScript import { AmityContentFlagReasonType, MessageRepository } from '@amityco/ts-sdk'; const flagged = await MessageRepository.flagMessage(messageId, { reason: AmityContentFlagReasonType.Others, detail: 'Contains sensitive account information', }); renderResults(flagged); ``` ```dart Flutter final flaggedMessage = await AmityChatClient.newMessageRepository() .flagMessage( messageId: messageId, reason: AmityContentFlagReason.others( 'Contains sensitive account information', ), ); final flagCount = flaggedMessage.flagCount; ``` The earlier calls that took only a reason are **deprecated** but still work on Android, iOS, and TypeScript: `flagMessage(messageId, reason)` with a reason string on TypeScript, and with `AmityContentFlagReason` on Android and iOS, where custom text was passed as `Others("...")` / `.others("...")`. Move custom text to `detail` with the Others reason. Flagging with only a message ID is not deprecated on these platforms. On Flutter, the repository `flag(messageId)` is deprecated; use `flagMessage(messageId:reason:)`. ## Unflag A Message Remove the current user's flag from a message when the user reverses the report action. ```kotlin Android messageRepository .unflagMessage(messageId = messageId) .subscribe( { showSuccessMessage() }, { error -> handleUnflagError(error) }, ) ``` ```swift iOS try await messageRepository.unflagMessage(withId: "message-id") showSuccessMessage("Message unflagged") ``` ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const unflagged = await MessageRepository.unflagMessage(messageId); renderResults(unflagged); ``` ```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. ```kotlin Android messageRepository .getMessage(messageId = messageId) .subscribe( { fetchedMessage -> val isFlaggedByMe = fetchedMessage.isFlaggedByMe() updateMessageUI(messageId, isFlaggedByMe) }, { error -> handleGeneralError(error) }, ) ``` ```swift iOS let isFlaggedByMe = try await messageRepository .isMessageFlaggedByMe(withId: "message-id") showSuccessMessage(isFlaggedByMe) ``` ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const isFlaggedByMe = await MessageRepository.isMessageFlaggedByMe(messageId); updateUI({ isFlaggedByMe }); ``` ```dart Flutter final fetchedMessage = await AmityChatClient.newMessageRepository() .getMessage(messageId); final isFlaggedByMe = fetchedMessage.isFlaggedByMe; ``` ## Platform Notes - Reason with optional detail ships in TypeScript 7.29.0 and in the October 2026 iOS and Android releases. - The Child endangerment reason ships in TypeScript 7.31.0 and in the next Android and iOS releases. Flutter's `AmityContentFlagReason` does not include it. - On iOS, when you omit `detail`, write the reason with its type, for example `AmityContentFlagReasonType.spamOrScams`. The shorthand `.spamOrScams` on its own selects the deprecated call. - A flag without a reason is recorded with a default reason: Others on TypeScript and Android, and Against community guidelines on iOS. - Flutter has no separate `detail`; the reporter's text travels on `AmityContentFlagReason.others(...)`. ## 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, sort order, 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` | `excludeArchives` on `AmityChannelQueryOptions` | `.excludeArchived(...)` | `.excludeArchives(...)` | | Sort | `sortBy` | `sortBy` with `.lastActivity` or `.lastMessage` | `.sortBy(...)` with `LAST_ACTIVITY` or `LAST_MESSAGE` | `.sortBy(...)` with `LAST_ACTIVITY` | | Known IDs | `channelIds` | `getChannels(channelIds:)` | `getChannels(channelIds)` | Not exposed in the current public repository | ## Parameters | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | | `types` | No | TypeScript, iOS, Android, Flutter | Channel types to include, such as community, live, or conversation. | | `membership` / `filter` | No | TypeScript, iOS, Android, Flutter | Restrict results by the current user's relationship to the channel. | | `tags` / `includingTags` | No | TypeScript, iOS, Android, Flutter | Include channels with specific app-defined tags. | | `excludeTags` / `excludingTags` | No | TypeScript, iOS, Android, Flutter | Exclude channels with specific app-defined tags. | | `isDeleted` / `includeDeleted` | No | TypeScript, iOS, Android, Flutter | Include or exclude deleted channels where the platform exposes the filter. | | `displayName` / `keyword` | No | TypeScript, Flutter | Search by name or keyword where supported by the platform SDK. | | `excludeArchives` | No | TypeScript, iOS, Android, Flutter | Exclude channels archived by the current user where supported. | | `sortBy` | No | TypeScript, iOS, Android, Flutter | Sort channel lists where the platform exposes sort options. Choose `lastActivity` (default) or `lastMessage`; see [Sort A Channel List](#sort-a-channel-list). | | Pagination controls | No | TypeScript, iOS, Android, Flutter | 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. ```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) .sortBy(AmityChannelSortBy.LAST_MESSAGE) .includingTags(includingTags) .excludingTags(excludingTags) .includeDeleted(includeDeleted = false) .build() .query() .subscribe( { pagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ```swift iOS let options = AmityChannelQueryOptions( types: Set([AmityChannelQueryType.community, AmityChannelQueryType.live]), filter: .userIsMember, includingTags: ["support"], excludingTags: ["hidden"], includeDeleted: false, sortBy: .lastMessage ) let channels = channelRepository.getChannels(with: options) token = channels.observe { collection, error in if let error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```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: 'lastMessage', }, ({ data: channels, onNextPage, hasNextPage, loading, error }) => { if (error) handleError(error); if (!loading && channels) { renderResults(channels); } if (hasNextPage) onNextPage?.(); }, ); unsubscribe(); ``` ```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(); ``` ## Sort A Channel List Use `sortBy` to choose how a channel list is ordered. Choose `lastMessage` for an inbox that should follow the conversation, so that joining or leaving a channel does not push it to the top. | Value | Order | What moves a channel to the top | | --- | --- | --- | | `lastActivity` (default) | Newest activity first | A new message, or a member being added, removed, or banned | | `lastMessage` | Newest message first | A new message. Membership changes do not reorder the list. | Each platform names the values in its own style: | Platform | Option | `lastActivity` | `lastMessage` | | --- | --- | --- | --- | | TypeScript | `sortBy` | `'lastActivity'` | `'lastMessage'` | | iOS | `AmityChannelQueryOptions.sortBy` (`AmityChannelSortBy`) | `.lastActivity` | `.lastMessage` | | Android | `.sortBy(...)` (`AmityChannelSortBy`) | `LAST_ACTIVITY` | `LAST_MESSAGE` | ```kotlin Android // Newest message first val disposable = channelRepository .getChannels() .all() .filter(AmityChannelFilter.MEMBER) .sortBy(AmityChannelSortBy.LAST_MESSAGE) .build() .query() .subscribe( { pagingData -> submitData(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ```swift iOS // Newest message first let options = AmityChannelQueryOptions( filter: .userIsMember, sortBy: .lastMessage ) let channels = channelRepository.getChannels(with: options) token = channels.observe { collection, error in if let error { handleError(error) return } for channel in collection.snapshots { showRow(name: channel.displayName, lastMessageAt: channel.lastMessageAt) } } ``` ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; // Newest message first const unsubscribe = ChannelRepository.getChannels( { membership: 'member', sortBy: 'lastMessage' }, ({ data: channels, error }) => { if (error) handleError(error); if (channels) { renderResults(channels); } }, ); unsubscribe(); ``` - **Live order.** The collection reorders as messages arrive. When the current user sends a message, that channel moves to the top right away, before the server responds. - **`lastMessageAt`.** The channel's `lastMessageAt` is the time of its newest message, or its creation time if it has none. Unlike `lastActivity`, membership changes and closing the channel do not change it. It is `null` (`nil` on iOS) when the backend has no value for the channel yet, and those channels are listed last, ordered by creation time. Read it as `channel.getLastMessageAt()` on Android, `channel.lastMessageAt` on iOS, and `channel.lastMessageAt` on TypeScript. - **Backend support.** `lastMessage` needs backend support for your network. Until it is available, the query fails with HTTP status 422 and error code `500000`. The SDK does not retry and does not fall back to `lastActivity`, so handle the error in your query callback. On iOS, `error.isAmityErrorCode(.business)` identifies it, and on TypeScript the code is `Amity.ServerError.BUSINESS_ERROR`. On Android, the error arrives as `LoadState.Error` in the paging load state, not in the `subscribe` error callback. `AmityError.from(error) == AmityError.BUSINESS_ERROR` identifies it. Flutter's `sortBy` currently accepts `LAST_ACTIVITY` only. On TypeScript, `sortBy` also accepts `lastCreated`, `firstCreated`, and `displayName`. ## 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. ```kotlin Android val disposable = channelRepository .getChannels() .conversationType() .includeDeleted(includeDeleted = false) .build() .query() .subscribe( { pagingData -> showSuccessMessage(pagingData) }, { error -> handleGeneralError(error) }, ) ``` ```swift iOS let options = AmityChannelQueryOptions( types: Set([AmityChannelQueryType.conversation]), filter: .userIsMember, includeDeleted: false ) let conversations = channelRepository.getChannels(with: options) showSuccessMessage(conversations) ``` ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unsubscribe = ChannelRepository.getChannels( { types: ['conversation'], membership: 'member', isDeleted: false, }, ({ data: channels }) => { renderResults(channels); }, ); unsubscribe(); ``` ```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` | `setNotificationMode(_:)` on `AmityChannelUpdateOptions` | `.notificationMode(...)` on the edit builder | `.notificationMode(...)` | ## Parameters | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | | `channelId` | Yes | TypeScript, iOS, Android, Flutter | Channel ID to update. | | `displayName` | No | TypeScript, iOS, Android, Flutter | Replacement channel display name. | | `avatarFileId` / `avatar` | No | TypeScript, iOS, Android, Flutter | Replacement avatar file reference where supported. | | `tags` | No | TypeScript, iOS, Android, Flutter | Replacement tag list for the channel. | | `metadata` | No | TypeScript, iOS, Android, Flutter | Replacement metadata stored with the channel. | | `notificationMode` | No | TypeScript, iOS, Android, Flutter | Per-channel notification mode: `default`, `silent`, or `subscribe`. Exposed on all four platforms. | ## 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 all four platforms. The available values are `default`, `silent`, and `subscribe`. Use the dedicated [channel notification settings](../../push-notifications/settings/channel-settings) 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); ``` ```swift iOS let options = AmityChannelUpdateOptions(channelId: "channel-id") options.setNotificationMode(.silent) let channel = try await channelRepository.editChannel(with: options) showSuccessMessage(channel.channelId) ``` ```kotlin Android val disposable = channelRepository .editChannel(channelId = channelId) .notificationMode(AmityChannelNotificationMode.SILENT) .build() .apply() .subscribe( { channel -> showSuccessMessage(channel.getChannelId()) }, { error -> handleGeneralError(error) }, ) ``` ```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. Archive APIs are available on TypeScript, iOS, Android, and Flutter. ## Platform Surface | Operation | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Archive | `ChannelRepository.archiveChannel(channelId)` | `channelRepository.archiveChannel(channelId:)` | `channelRepository.archiveChannel(channelId)` | `archiveChannel(channelId)` | | Unarchive | `ChannelRepository.unarchiveChannel(channelId)` | `channelRepository.unarchiveChannel(channelId:)` | `channelRepository.unarchiveChannel(channelId)` | `unarchiveChannel(channelId)` | | Query archived channels | `ChannelRepository.getArchivedChannels(...)` | `getArchivedChannels()` or `getArchivedChannelIds()` | `getArchivedChannels()` or `getArchivedChannelIds()` | `getArchivedChannels()` | ## Parameters | Operation | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | --- | | Archive | `channelId` | Yes | TypeScript, iOS, Android, Flutter | Channel ID to hide from the current user's active channel list. | | Unarchive | `channelId` | Yes | TypeScript, iOS, Android, Flutter | Archived channel ID to restore to the active channel list. | | Query archived channels | `limit` | No | TypeScript | TypeScript collection page size. | | Query archived channels | Pagination controls | No | TypeScript, iOS, Flutter | Use TypeScript collection callbacks, `nextPage()` on the iOS `AmityCollection`, or Flutter live collection loading. Android's `getArchivedChannels()` emits the full list on each update, so there is nothing to page. | ## 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); ``` ```swift iOS try await channelRepository.archiveChannel(channelId: "channel-id") try await channelRepository.unarchiveChannel(channelId: "channel-id") ``` ```kotlin Android channelRepository.archiveChannel(channelId = channelId) .subscribe() channelRepository.unarchiveChannel(channelId = channelId) .subscribe() ``` ```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(); ``` ```swift iOS // A live collection of archived channels. let archivedChannels = channelRepository.getArchivedChannels() token = archivedChannels.observe { collection, error in renderResults(collection.snapshots) } // Or just the archived channel IDs. let ids = try await channelRepository.getArchivedChannelIds() ``` ```kotlin Android // A stream of archived channels. channelRepository.getArchivedChannels() .subscribe( { channels: List -> renderResults(channels) }, { error -> handleGeneralError(error) }, ) // Or just the archived channel IDs. channelRepository.getArchivedChannelIds() .subscribe( { channelIds: List -> renderResults(channelIds) }, { error -> handleGeneralError(error) }, ) ``` ```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. | | Query shape | Every platform returns archived channels as a collection or stream. iOS and Android also expose an IDs-only variant, `getArchivedChannelIds()`. | ## 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.plus 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. --- ### [Pin and Unpin Messages](https://learn.social.plus/social-plus-sdk/chat/messaging-features/messages/pin-message) > Pin one message in a livestream chat channel, unpin it, and keep the pinned message in sync through the channel live object and realtime events. Use pin APIs to surface one important message above a livestream chat so every viewer sees it, no matter how fast the feed scrolls. Pin state is owned by the server and exposed on the channel as `pinnedMessage`. A channel holds at most one pinned message at a time. Pinning is available on `live` channels only, the channel type attached to a livestream room. The server rejects pin requests on conversation, community, broadcast, standard, and private channels. The Flutter and React Native SDKs do not ship pin support. ## Platform Surface | Operation | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Pin a message | `MessageRepository.pinMessage(messageId)` | `messageRepository.pinMessage(withId:)` | `messageRepository.pinMessage(messageId)` | Not supported | | Unpin a message | `MessageRepository.unpinMessage(messageId)` | `messageRepository.unpinMessage(withId:)` | `messageRepository.unpinMessage(messageId)` | Not supported | | Read the pinned message | `channel.pinnedMessage` | `channel.pinnedMessage` | `channel.getPinnedMessage()` | Not supported | | Pin realtime events | `ChannelRepository.onChannelMessagePinned(...)`, `ChannelRepository.onChannelMessageUnpinned(...)` | Delivered through the channel live object | Delivered through the channel live object | Not supported | | Permission constant | `'PIN_MESSAGE'` (`Amity.Permission.PinMessagePermission`) | `AmityPermission.pinMessage` | `AmityPermission.PIN_MESSAGE` | Not supported | ## Parameters | Operation | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | --- | | Pin | `messageId` | Yes | TypeScript, iOS, Android | ID of the message to pin. Use the same `messageId` you pass to other message repository functions. | | Unpin | `messageId` | Yes | TypeScript, iOS, Android | ID of the message that is currently pinned. Take it from `pinnedMessage.messageId`. The server rejects the call if this message is not the current pin. | | Permission check | `channelId` | Yes | TypeScript, iOS, Android | Channel to check the current user's `PIN_MESSAGE` permission in. | ## Who Can Pin The server authorizes pin and unpin with the `PIN_MESSAGE` channel permission. Default moderator roles carry it, so channel moderators can pin. A livestream host and co-hosts receive the channel moderator role when the room is created or when a co-host joins, so they can pin as well. Network admins with the moderate-channel admin permission can pin from the Console. A message author cannot pin their own message by authorship alone. Only role holders can pin. Check the permission before you show a pin control: ```typescript TypeScript import { Client } from '@amityco/ts-sdk'; const client = Client.createClient('your-api-key', 'sg'); // A network-level grant (Console) shows up on the user; a channel role shows up on the channel. const permission = client.hasPermission('PIN_MESSAGE'); const canPin = permission.currentUser() || permission.channel(channelId); if (canPin) { // Show the pin control in the message action menu. } ``` ```swift iOS Task { @MainActor in let canPin = await client.hasPermission( .pinMessage, forChannel: "channel-id" ) if canPin { // Show the pin control in the message action menu. } } ``` ```kotlin Android fun checkPinPermission(channelId: String) { AmityCoreClient.hasPermission(AmityPermission.PIN_MESSAGE) .atChannel(channelId) .check() .doOnNext { canPin: Boolean -> if (canPin) { // Show the pin control in the message action menu. } } .subscribe() } ``` On Android the check is a `Flowable` and re-emits when the user's channel membership changes. On iOS the check is one-shot: re-run it when the channel, room, the current user's channel membership, or the current user's own record updates, and after a `403`. Permission changes reach the client over realtime events by scope: a **channel-level** role change (moderator or co-host) over the `channel.roleAdded` / `channel.roleRemoved` events, which update the user's channel permissions; and a **network-level** change made from the Console over the `user.updated` event, which updates the user's network permissions. On iOS, `user.updated` is delivered only while the current user's events are subscribed (`AmityUser.subscribeEvent(.user)`); the pre-built livestream chat UI subscribes this automatically while it is open, and unsubscribes when it closes. Because the permission check combines the user's network and channel permissions, re-checking on either update reflects a mid-stream change with no user action, and the `403` re-check is a backstop. Roles are seeded when a network is created. On a network created before pin support shipped, existing roles may not include `PIN_MESSAGE` until the role backfill has run for that network. Until then the permission check returns `false` and every pin request returns `403`. Contact support if pin controls never appear on an older network. ## Pin A Message Pin a message in its livestream channel. If another message is already pinned, the new pin replaces it in one step. You do not need to unpin first. The SDK calls `POST /api/v5/messages/{messageId}/pin`. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const channel = await MessageRepository.pinMessage(messageId); renderResults(channel.pinnedMessage); ``` ```swift iOS do { let channel = try await messageRepository.pinMessage(withId: "message-id") showSuccessMessage(channel.pinnedMessage?.messageId) } catch { handleError(error) } ``` ```kotlin Android val disposable = messageRepository .pinMessage(messageId = messageId) .subscribe( { channel -> showSuccessMessage(channel.getPinnedMessage()?.getMessageId()) }, { error -> handleGeneralError(error) }, ) ``` The call returns the updated channel with the pinned message set. The SDK writes the channel into its cache, so any active channel observer for that channel fires immediately, without waiting for the realtime event. Pinning the message that is already pinned returns a `400` error. Pinning a deleted message, or a message whose author is banned from the channel or globally banned, also returns `400`. Pin state is unchanged on any error. ## Unpin A Message Remove the current pin. Pass the `messageId` of the message that is currently pinned. The SDK calls `DELETE /api/v5/messages/{messageId}/pin`. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; const channel = await MessageRepository.unpinMessage(messageId); renderResults(channel.pinnedMessage); // null ``` ```swift iOS do { let channel = try await messageRepository.unpinMessage(withId: "message-id") showSuccessMessage(channel.pinnedMessage == nil) } catch { handleError(error) } ``` ```kotlin Android val disposable = messageRepository .unpinMessage(messageId = messageId) .subscribe( { channel -> showSuccessMessage(channel.getPinnedMessage() == null) }, { error -> handleGeneralError(error) }, ) ``` The server rejects an unpin with `400` when `messageId` is not the current pin. This is deliberate. A stale client cannot clear a pin that someone else has since replaced. When you receive that error, re-read the channel or rely on the next `channel.messagePinned` event to get the current pin. Unpin is allowed regardless of stream state, so a moderator can clean up after a stream ends. ## Read The Pinned Message The pinned message lives on the channel, so observe the channel you already hold for the live chat. The field is `null` when nothing is pinned. On TypeScript it is typed optional, so a channel payload that omits it reads as `undefined`. Treat `null` and `undefined` the same way, as the `?? null` in the example does. ```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.pinnedMessage ?? null); } }, ); 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 } if let pinned = channel.pinnedMessage { showSuccessMessage(pinned.data?["text"] as? String) } else { // Nothing is pinned. Hide the banner. } } ``` ```kotlin Android val disposable = channelRepository .getChannel(channelId = channelId) .subscribe( { channel -> val pinned: AmityPinnedMessage? = channel.getPinnedMessage() if (pinned != null) { // getData() returns AmityMessage.Data. Cast it to read the text. val text = (pinned.getData() as? AmityMessage.Data.TEXT)?.getText() showSuccessMessage(text) } else { // Nothing is pinned. Hide the banner. } }, { error -> handleGeneralError(error) }, ) ``` ### Pinned message shape The pinned message carries a snapshot of the message body plus who pinned it and when. It is not a full message object, so use the fields below rather than message-repository helpers. Android exposes each field through the getter named in the third column. | Field | Type | Android getter | Description | | --- | --- | --- | --- | | `messageId` | string | `getMessageId()` | ID of the pinned message. Pass this to the unpin call. | | `parentId` | string \| null | `getParentId()` | Set only when the pinned message is a reply. | | `channelId` | string | `getChannelId()` | Internal channel ID. | | `channelPublicId` | string | `getChannelPublicId()` | Public channel ID, the value you use with the channel repository. | | `messageFeedId` | string | `getSubChannelId()` | Subchannel the message belongs to. | | `segment` | number | `getSegment()` | Message segment within the subchannel. | | `dataType` | string | `getDataType()` | Message data type. Livestream chat pins text messages. On iOS it is the `AmityMessageType` enum; on Android the getter returns the `AmityMessage.DataType` enum. | | `data` | object | `getData()` | Message data. For text messages, `{ text }`. On Android the getter returns `AmityMessage.Data`. Cast it to `AmityMessage.Data.TEXT` and call `getText()`. | | `creatorId` | string | `getCreatorId()` | Internal ID of the message author. | | `creatorPublicId` | string | `getCreatorPublicId()` | Public user ID of the message author. Use it to load the author profile. | | `isDeleted` | boolean | `isDeleted()` | Always `false` in practice. The server clears the pin when the message is deleted. | | `pinnedBy` | string | `getPinnedBy()` | Internal ID of the user who pinned the message. | | `pinnedAt` | date-time | `getPinnedAt()` | When the message was pinned. | | `createdAt`, `updatedAt` | date-time | `getCreatedAt()`, `getUpdatedAt()` | Message timestamps. | The response and the pin event include the pinned message author in their `users` payload, so the author is usually already in the user cache when you render the banner. ## Listen For Pin Changes Two realtime events arrive on the channel topic that live chat clients already subscribe to. You do not need an extra topic subscription. | Event | When it fires | Payload | | --- | --- | --- | | `channel.messagePinned` | A message is pinned, including when a new pin replaces the current one | Full snapshot: `channelId`, `pinnedMessage`, `users` | | `channel.messageUnpinned` | The pin is removed, either by a moderator or automatically | `channelId` only | On every platform the SDK handles both events internally: it updates the cached channel and the channel live object emits. The TypeScript SDK also exposes the two events as callbacks for cases where you need a hook separate from the channel observer. On iOS and Android there is no separate callback. Observe the channel and compare the pinned message ID between emissions to tell a pin from an unpin. ```typescript TypeScript import { ChannelRepository } from '@amityco/ts-sdk'; const unsubscribePinned = ChannelRepository.onChannelMessagePinned((channel) => { renderResults(channel.pinnedMessage); }); const unsubscribeUnpinned = ChannelRepository.onChannelMessageUnpinned((channel) => { renderResults(channel.pinnedMessage); // null }); unsubscribePinned(); unsubscribeUnpinned(); ``` ```swift iOS var currentPinnedMessageId: String? 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 } let pinned = channel.pinnedMessage switch (currentPinnedMessageId, pinned?.messageId) { case (nil, let newId?): // channel.messagePinned: show the banner. showSuccessMessage("Pinned \(newId)") case (let oldId?, let newId?) where oldId != newId: // channel.messagePinned with a replacement: re-render the banner collapsed. showSuccessMessage("Replaced \(oldId) with \(newId)") case (_?, nil): // channel.messageUnpinned, or the pinned message was deleted: hide the banner. showSuccessMessage("Unpinned") default: break } currentPinnedMessageId = pinned?.messageId } ``` ```kotlin Android var currentPinnedMessageId: String? = null val disposable = channelRepository .getChannel(channelId = channelId) .subscribe( { channel -> val newId = channel.getPinnedMessage()?.getMessageId() val oldId = currentPinnedMessageId when { oldId == null && newId != null -> { // channel.messagePinned: show the banner. showSuccessMessage("Pinned $newId") } oldId != null && newId != null && oldId != newId -> { // channel.messagePinned with a replacement: re-render the banner collapsed. showSuccessMessage("Replaced $oldId with $newId") } oldId != null && newId == null -> { // channel.messageUnpinned, or the pinned message was deleted: hide the banner. showSuccessMessage("Unpinned") } } currentPinnedMessageId = newId }, { error -> handleGeneralError(error) }, ) ``` Realtime delivery on iOS and Android requires the channel topic to be subscribed, which the live chat already does for the channel it renders. If you observe a channel outside a live chat screen, subscribe to its topic first as described in [Chat Realtime Events](/social-plus-sdk/core-concepts/realtime-communication/realtime-events/chat-realtime-events). Treat every `messagePinned` payload as the full current state and apply the last one you receive. Do not treat it as an increment. Replacing a pin emits one `messagePinned` event with the new message, not an unpin followed by a pin. ### Automatic unpin The server clears the pin and emits `channel.messageUnpinned` when: - The pinned message is deleted. - The pinned message author is banned from the channel or globally banned. The author's messages are deleted as part of the ban. A community ban does not affect the pin. The SDK also clears the cached pinned message when it receives a `message.deleted` event for the pinned message, so the channel stays correct even if the unpin event is missed. Realtime events are not replayed after a reconnect. A client that was offline while the pin changed receives nothing on reconnect. Read the channel again after reconnecting and use the channel's pinned message as the source of truth. ## Server Rules And Errors | Condition | HTTP | `code` | | --- | --- | --- | | Caller lacks `PIN_MESSAGE` and is not an admin with the moderate-channel permission | `403` | `400301`. Permission changes also arrive over realtime events — `channel.roleAdded` / `channel.roleRemoved` for a channel-level role change, and `user.updated` for a network-level change made from the Console — which update the caller's permissions, so a revoked permission is usually reflected before a pin attempt. The TypeScript SDK additionally refreshes the caller's permissions on the `403` itself, as a backstop. | | Message or channel not found | `404` | `400400` | | Message is deleted | `400` | `400000` | | Message is already pinned (pin) | `400` | `400000` | | Message is not the current pin (unpin) | `400` | `400000` | | Message author is channel-banned or globally banned (pin) | `400` | `400000` | | Channel type is not `live` (pin) | `400` | `400000` | Every `400` condition shares the code `400000`. Only the server `message` string tells them apart, so do not branch on the code to decide which one happened. Pin state is never changed by a failed request, so a client does not need to roll anything back. Re-read the channel, or wait for the next event, and render what it says. ```typescript TypeScript import { MessageRepository } from '@amityco/ts-sdk'; try { await MessageRepository.pinMessage(messageId); } catch (error) { // The pin did not change. Hide the pin control after a 403, // and let the channel observer keep the banner in sync. handleError(error); } ``` ```swift iOS do { _ = try await messageRepository.pinMessage(withId: "message-id") } catch { // The pin did not change. Re-check hasPermission(.pinMessage, forChannel:) // after a 403, and let the channel observer keep the banner in sync. handleError(error) } ``` ```kotlin Android val disposable = messageRepository .pinMessage(messageId = messageId) .subscribe( { /* Channel observer renders the new pin. */ }, { error -> // The pin did not change. The permission Flowable re-emits after a 403, // and the channel observer keeps the banner in sync. handleGeneralError(error) }, ) ``` ## Related Topics Observe the channel live object that carries the pinned message. Check `PIN_MESSAGE` and other channel permissions before showing controls. Ready-made pin action and pinned message banner in the livestream chat. ## 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 | | `needApprovalOnCommentCreation` (TypeScript) / `isCommentReviewEnabled` (iOS) / `isCommentReviewEnabled()` (Android) | Boolean | Whether members' comments are held for moderator review. Not exposed on Flutter. See [Comment Review](/social-plus-sdk/social/content-management/comments/moderation/comment-review) | | `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 | | `needApprovalOnCommentCreation` / `setNeedApprovalOnCommentCreation(_:)` / `isCommentReviewEnabled` | TypeScript / iOS / Android | Hold members' comments for moderator review. Defaults to `false`. See [Comment Review](/social-plus-sdk/social/content-management/comments/moderation/comment-review) | | `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` | ## Comment Review Option Comment review is a separate community setting from post review. When it is on, comments on the community's posts are held until a reviewer approves them, unless their author can review the community's comments. | Concept | TypeScript | iOS | Android | Flutter | |---------|------------|-----|---------|---------| | Set on create or update | `needApprovalOnCommentCreation: true` | `setNeedApprovalOnCommentCreation(true)` | `isCommentReviewEnabled(true)` on the builder | Not exposed | | Read on the community | `community.needApprovalOnCommentCreation` | `community.isCommentReviewEnabled` | `community.isCommentReviewEnabled()` | Not exposed | On Flutter, turn comment review on from the Console, another SDK, or the server API. For who is held, the review queue, and approve or decline, see [Comment Review](/social-plus-sdk/social/content-management/comments/moderation/comment-review). ## 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, comment review 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 | | `needApprovalOnCommentCreation` / `setNeedApprovalOnCommentCreation(_:)` / `isCommentReviewEnabled` | TypeScript / iOS / Android | Turn comment review on or off. Turning it off does not clear comments already waiting for review. See [Comment Review](/social-plus-sdk/social/content-management/comments/moderation/comment-review) | | `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 | Platforms | Description | |-----------|-----------|-------------| | `communityId` | All | Community to query. | | `roles` | All | Optional role IDs to include, such as `community-moderator`. | | `includeDeleted` | All | Include deleted users. | | `excludingRoles` | TypeScript, iOS, Android | Exclude members holding any role in the list. Applies to both query and search. | | `limit` / pagination token | TypeScript, Flutter | Explicit page size. iOS and Android paginate through the live collection and `PagingData` instead. | ### 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. It is available in TypeScript, iOS, and Android, on both the member query and the member search APIs. Flutter exposes `excludingRoles` on neither builder. How it resolves: - A member is removed if they hold **any** role in the list — not only if they hold all of them. - An **empty list applies no exclusion**, so you can pass one unconditionally and let the caller decide. iOS and Android default the parameter to an empty list; in TypeScript it is optional. - `roles` and `excludingRoles` combine as an intersection: a member is returned only if they match `roles` and hold none of the roles in `excludingRoles`. Listing the same role in both therefore yields no members. ```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.plus 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. Deprecated with comment approval (iOS/TS) — use `getLocalCommentCount(includePending:)`. | 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.plus 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 posts and comments with an optional reason and detail, unflag them, and check flag status. Use content flagging APIs when users need to report posts or comments for review. A report can say _why_ with a reason, and add the reporter's own words as an optional detail, so moderators see the context without a follow-up. The SDK supports flagging, unflagging, and checking whether the current user has flagged a loaded item. TypeScript, iOS, and Android flag posts and comments with no reason, or with a reason and an optional detail. Flutter flags posts and comments without a reason; its public post and comment flag builders do not accept a reason yet. ## 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. Pick one predefined reason and optionally add the reporter's own words as detail, for any reason including Others. Check whether the current user has flagged a loaded post or comment before rendering an unflag action. ## Flag reasons A reason is one fixed category. On Android, iOS, and TypeScript it never carries text: the reporter's own words always go in `detail`. Flutter uses these reasons for messages only, and its Others reason carries the text. | Reason | Android `AmityContentFlagReasonType` | iOS `AmityContentFlagReasonType` | TypeScript `AmityContentFlagReasonType` | Flutter `AmityContentFlagReason` (messages only) | | --- | --- | --- | --- | --- | | Against community guidelines | `COMMUNITY_GUIDELINES` | `.communityGuidelines` | `CommunityGuidelines` | `communityGuidelines` | | Child endangerment | `CHILD_ENDANGERMENT` | `.childEndangerment` | `ChildEndangerment` | Not available | | Harassment or bullying | `HARASSMENT_OR_BULLYING` | `.harassmentOrBullying` | `HarassmentOrBullying` | `harassmentOrBullying` | | Self-harm or suicide | `SELF_HARM_OR_SUICIDE` | `.selfHarmOrSuicide` | `SelfHarmOrSuicide` | `selfHarmOrSuicide` | | Violence or threatening content | `VIOLENCE_OR_THREATENING_CONTENT` | `.violenceOrThreateningContent` | `ViolenceOrThreateningContent` | `violenceOrThreateningContent` | | Selling and promoting restricted items | `SELLING_RESTRICTED_ITEMS` | `.sellingRestrictedItems` | `SellingRestrictedItems` | `sellingRestrictedItems` | | Sexual content or nudity | `SEXUAL_CONTENT_OR_NUDITY` | `.sexualContentOrNudity` | `SexualContentOrNudity` | `sexualContentOrNudity` | | Spam or scams | `SPAM_OR_SCAMS` | `.spamOrScams` | `SpamOrScams` | `spamOrScams` | | False information or misinformation | `FALSE_INFORMATION` | `.falseInformation` | `FalseInformation` | `falseInformation` | | Others | `OTHERS` | `.others` | `Others` | `others('...')` | Use **Child endangerment** for content that endangers a child, including suspected child sexual abuse material (CSAM). Send it as its own reason, not as Others with a detail, so these reports reach moderators under their own category. The reason is sent as the exact text `Child endangerment`. On TypeScript, `AmityContentFlagReasonType` is the same enum as `ContentFlagReasonEnum`, exported under the name Android and iOS use. Both names work. Before TypeScript 7.31.0, only `ContentFlagReasonEnum` is exported. ## Parameters | Operation | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | --- | | Flag a post or comment | `postId` / `commentId` | Yes | All | ID of the content to report. | | Flag a post or comment | `reason` | No | Android, iOS, TypeScript | One reason from the table above. Omit it to flag without a reason. | | Flag a post or comment | `detail` | No | Android, iOS, TypeScript | The reporter's own words, for any reason including Others. Needs a `reason`. Sent as given; omitted when not set. The server limits it to 300 characters. | | Unflag content | `postId` / `commentId` | Yes | All | Content ID whose current-user report should be removed. | | Check flag status | `postId` / `commentId` or loaded model | Yes | All | Checks whether the current user has flagged the item. | A `detail` without a `reason` is not possible: every call that takes `detail` also requires `reason`. On TypeScript, a malformed options object is rejected before any request is sent. ## Flag a post Flag a post when a member reports it. Pass a reason when your report menu offers one, and pass the reporter's text as `detail` when they add some. Use the no-reason call when your UI only offers a single "Report" action. ```kotlin Android val postRepository = AmitySocialClient.newPostRepository() // Reason with the reporter's own words postRepository .flagPost( postId = postId, reason = AmityContentFlagReasonType.SPAM_OR_SCAMS, detail = "Repeated promotional links", ) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) // Reason only postRepository .flagPost(postId, AmityContentFlagReasonType.HARASSMENT_OR_BULLYING) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) // No reason postRepository .flagPost(postId = postId) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```swift iOS let repository = AmityPostRepository() // Reason with the reporter's own words try await repository.flagPost( withId: "post-id", reason: .spamOrScams, detail: "Repeated promotional links" ) // Reason only: type the reason explicitly so the current call is used try await repository.flagPost( withId: "post-id", reason: AmityContentFlagReasonType.harassmentOrBullying ) // No reason try await repository.flagPost(withId: "post-id") ``` ```typescript TypeScript import { AmityContentFlagReasonType, PostRepository } from '@amityco/ts-sdk'; // Reason with the reporter's own words const didFlag = await PostRepository.flagPost(postId, { reason: AmityContentFlagReasonType.SpamOrScams, detail: 'Repeated promotional links', }); if (didFlag) { showSuccessMessage('Post flagged'); } // Reason only await PostRepository.flagPost(postId, { reason: AmityContentFlagReasonType.HarassmentOrBullying }); // No reason await PostRepository.flagPost(postId); ``` ```dart Flutter // Flutter flags posts without a reason final didFlag = await post.report().flag(); if (didFlag) { // Show flagged state. } ``` ## Flag a comment Flag a comment the same way: no reason, or a reason with an optional `detail`. ```kotlin Android val commentRepository = AmitySocialClient.newCommentRepository() // Reason with the reporter's own words commentRepository .flagComment( commentId = commentId, reason = AmityContentFlagReasonType.HARASSMENT_OR_BULLYING, detail = "Insults another member in every reply", ) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) // No reason commentRepository .flagComment(commentId = commentId) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```swift iOS let repository = AmityCommentRepository() // Reason with the reporter's own words try await repository.flagComment( withId: "comment-id", reason: .harassmentOrBullying, detail: "Insults another member in every reply" ) // No reason; returns a Bool let didFlag = try await repository.flagComment(withId: "comment-id") ``` ```typescript TypeScript import { AmityContentFlagReasonType, CommentRepository } from '@amityco/ts-sdk'; // Reason with the reporter's own words const didFlag = await CommentRepository.flagComment(commentId, { reason: AmityContentFlagReasonType.HarassmentOrBullying, detail: 'Insults another member in every reply', }); if (didFlag) { showSuccessMessage('Comment flagged'); } // No reason await CommentRepository.flagComment(commentId); ``` ```dart Flutter // Flutter flags comments without a reason final didFlag = await comment.report().flag(); if (didFlag) { // Show flagged state. } ``` ## Report child endangerment Flag content that endangers a child with the Child endangerment reason. A `detail` is optional, as for any reason. The same reason works for comments and messages, and for user behavior reports. {/* doc-as-test: skip (CHILD_ENDANGERMENT ships after the published Android SDK) */} ```kotlin Android AmitySocialClient.newPostRepository() .flagPost(postId, AmityContentFlagReasonType.CHILD_ENDANGERMENT) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` {/* doc-as-test: skip (childEndangerment ships after the published iOS SDK) */} ```swift iOS // Type the reason explicitly so the current call is used try await AmityPostRepository().flagPost( withId: "post-id", reason: AmityContentFlagReasonType.childEndangerment ) ``` ```typescript TypeScript import { AmityContentFlagReasonType, PostRepository } from '@amityco/ts-sdk'; await PostRepository.flagPost(postId, { reason: AmityContentFlagReasonType.ChildEndangerment, }); ``` ## Build a report-reason picker On TypeScript and iOS, build your report menu from the SDK's reason list instead of a hardcoded copy. A reason added in a later SDK release then appears in your menu without a code change. | | TypeScript | iOS | | --- | --- | --- | | Reasons in display order, Others last | `getContentFlagReasons()` (a new array on each call) | `AmityContentFlagReasonType.allCases` | | Stable key, such as `child_endangerment` | `getContentFlagReasonKey(reason)` | `reason.key` | | English label | the enum value | `reason.defaultLabel` | The key is snake_case, the same on every platform, and never renamed. Use it to look up your translated label, and show the English label when you have no translation for the key. Never send the key or the label to the server. Pass the enum value as `reason`. ```typescript TypeScript import { AmityContentFlagReasonType, getContentFlagReasonKey, getContentFlagReasons, } from '@amityco/ts-sdk'; // Your app's translated strings, keyed by your own prefix plus the SDK key declare const translations: Record; const reasonRows = getContentFlagReasons() // Offer Others separately, with a free-text field for detail .filter((reason) => reason !== AmityContentFlagReasonType.Others) .map((reason) => ({ reason, label: translations[`report_reason_${getContentFlagReasonKey(reason)}`] ?? reason, })); ``` {/* doc-as-test: skip (key and defaultLabel ship after the published iOS SDK) */} ```swift iOS // Your app's translated strings, keyed by your own prefix plus the SDK key let translations: [String: String] = [:] let reasonRows = AmityContentFlagReasonType.allCases // Offer Others separately, with a free-text field for detail .filter { $0 != .others } .map { reason in (reason: reason, label: translations["report_reason_\(reason.key)"] ?? reason.defaultLabel) } ``` Android has no key or label helpers yet. To list the reasons there, iterate `AmityContentFlagReasonType.entries` (or `values()` on Kotlin earlier than 1.9) and supply your own label for each value. ## Report with Others Use the Others reason when none of the categories fit. Put the reporter's explanation in `detail`, exactly as for any other reason. ```kotlin Android AmitySocialClient.newPostRepository() .flagPost( postId = postId, reason = AmityContentFlagReasonType.OTHERS, detail = "Shares a member's home address", ) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```swift iOS try await AmityPostRepository().flagPost( withId: "post-id", reason: .others, detail: "Shares a member's home address" ) ``` ```typescript TypeScript import { AmityContentFlagReasonType, PostRepository } from '@amityco/ts-sdk'; await PostRepository.flagPost(postId, { reason: AmityContentFlagReasonType.Others, detail: "Shares a member's home address", }); ``` The earlier calls that took only a reason are **deprecated** but still work: `flagPost(postId, reason)` and `flagComment(commentId, reason)` with a reason string on TypeScript, and with `AmityContentFlagReason` on Android and iOS, where custom text was passed as `Others("...")` / `.others("...")`. Move custom text to `detail` with the Others reason. Flagging with only an ID is not deprecated. ## Unflag content Unflag content when the current user removes their own report from a post or comment. ```kotlin Android AmitySocialClient.newPostRepository() .unflagPost(postId = postId) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) AmitySocialClient.newCommentRepository() .unflagComment(commentId = commentId) .subscribe( { showSuccessMessage() }, { error -> handleGeneralError(error) }, ) ``` ```swift iOS let postRepository = AmityPostRepository() let commentRepository = AmityCommentRepository() try await postRepository.unflagPost(withId: "post-id") try await commentRepository.unflagComment(withId: "comment-id") ``` ```typescript TypeScript import { CommentRepository, PostRepository } from '@amityco/ts-sdk'; const didUnflagPost = await PostRepository.unflagPost(postId); const didUnflagComment = await CommentRepository.unflagComment(commentId); ``` ```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. ```kotlin Android val loadedPost = post ?: return val loadedComment = comment ?: return val isPostFlagged = loadedPost.isFlaggedByMe() val isCommentFlagged = loadedComment.isFlaggedByMe() ``` ```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") ``` ```typescript TypeScript import { CommentRepository, PostRepository } from '@amityco/ts-sdk'; const isPostFlagged = await PostRepository.isPostFlaggedByMe(postId); const isCommentFlagged = await CommentRepository.isCommentFlaggedByMe(commentId); ``` ```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. ## Platform notes - Reason with optional detail ships in TypeScript 7.29.0 and in the October 2026 iOS and Android releases. - The Child endangerment reason ships in TypeScript 7.31.0, which also adds `getContentFlagReasons()`, `getContentFlagReasonKey()`, and the `AmityContentFlagReasonType` export. Android and iOS add the reason in their next releases; the iOS release also adds `key` and `defaultLabel` to `AmityContentFlagReasonType`. Flutter does not offer it. - On iOS, when you omit `detail`, write the reason with its type, for example `AmityContentFlagReasonType.spamOrScams`. The shorthand `.spamOrScams` on its own selects the deprecated call. - A flag without a reason is recorded with a default reason: Others on TypeScript and Android, and Against community guidelines on iOS. - Flutter flags posts and comments without a reason. ## 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. Flag chat messages with the same reasons and detail. ## 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.plus. 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. | | Event | Supported by TypeScript, iOS, and Android. References an existing event via `data.eventId`. Reads must go through `GET /api/v5/posts` — event posts are excluded from unfiltered `v3` and `v4` results. See [Event Posts](./creation/event-post). | | 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`, `event`, 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`, `EVENT`, `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. **Deprecated with comment approval.** In the comment-approval SDKs (iOS and TypeScript), `localCommentCount` is deprecated in favor of `getLocalCommentCount(includePending:)`. The old property equals `getLocalCommentCount(includePending: true)` — it includes the author's own pending comment; pass `includePending: false` for the shared, approved-only count. Move new code to `getLocalCommentCount(includePending:)` and choose the flag explicitly. See [Comment Review](/social-plus-sdk/social/content-management/comments/moderation/comment-review#related-sdk-functions) for how the two counts behave under review. ```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. Publish an event as a post so it renders as a card in feeds. 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.plus 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. The text is the parent post content. The SDK creation fields below describe the SDK API; UIKit's product-tag UI is a separate surface and may support fewer post types. See [UIKit Product Tagging](/uikit/components/social/product-tagging) for the UI-specific coverage. 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 | Platforms | Description | | --- | --- | --- | --- | | `text` | Yes | TypeScript, iOS, Android, Flutter | Text content for the post body. | | `targetType` | Yes for explicit feed targets | TypeScript, iOS, Android, Flutter | Feed target, usually `community` or `user`. | | `targetId` | Yes for explicit feed targets | TypeScript, iOS, Android, Flutter | Community ID or user ID for the target feed. | | `metadata` | No | TypeScript, iOS, Android, Flutter | Custom metadata stored with the post where supported. | | `mentionees` | No | TypeScript, iOS, Android, Flutter | User mention payload where supported by the platform builder. | | `links` | No | TypeScript, Android, iOS | Structured link metadata supported by TypeScript, Android, and iOS. | | `productTags` | No | TypeScript, Android, iOS | Products tagged in the post text. Text product tag types: `Amity.TextProductTag`, `AmityTextProductTag`, `AmityProductTag.Text`. | **Product tags (SDK API).** `productTags` tags products mentioned in the post **text**. Each tag names a product and the character range it covers in that text. Media product tags do not apply to a text-only post. When a post also has media, the text remains on the parent and the media is a child; use the media page's `attachmentProductTags` for that child. See [Image Post](./image-post) or [Video Post](./video-post) for those fields. Flutter does not expose product tags on any post builder. For tagging limits and tap-through behavior, see [Product Tagging](/uikit/components/social/posts#product-tagging). ## 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, }, ], }); ``` ## Tag Products in the Text TypeScript, Android, and iOS accept a `productTags` array when creating a text post. Each tag names a product and the character range it covers in the post text, so UIKit can render that range as a tappable product mention. Set `index` and `length` against the same string you pass as `text`. In the examples below, `featured product` starts at index 12 and is 16 characters long. iOS stores only the range. TypeScript and Android also carry the matched `text`. ```swift iOS let postRepository = AmityPostRepository() let builder = AmityTextPostBuilder() builder.setText("Loving this featured product for my morning runs!") let post = try await postRepository.createTextPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil, productTags: [ AmityTextProductTag(productId: "prod_001", index: 12, length: 12) ] ) ``` ```kotlin Android postRepository.createTextPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, text = "Loving this featured product for my morning runs!", productTags = listOf( AmityProductTag.Text( productId = "prod_001", text = "featured product", index = 12, length = 16 ) ) ) .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: "Loving this featured product for my morning runs!", }, productTags: [ { productId: "prod_001", text: "featured product", index: 12, length: 16 }, ], }); ``` ## 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.plus 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 | Platforms | Description | | --- | --- | --- | --- | | `images`, `uploadedImages`, or `fileId` | Yes | TypeScript, iOS, Android, Flutter | Uploaded image objects or image file IDs, depending on SDK. | | `text` | No | TypeScript, iOS, Android, Flutter | Caption text shown with the image post. | | `targetType` | Yes | TypeScript, iOS, Android, Flutter | Feed target, usually `community` or `user`. | | `targetId` | Yes | TypeScript, iOS, Android, Flutter | Community ID or user ID for the target feed. | | `metadata` | No | TypeScript, iOS, Android, Flutter | Custom metadata stored with the post where supported. | | `mentionees` | No | TypeScript, iOS, Android, Flutter | User mention payload where supported by the platform builder. | | `productTags` | No | TypeScript, Android, iOS | Products tagged in the post text. Text product tag types: `Amity.TextProductTag`, `AmityTextProductTag`, `AmityProductTag.Text`. | | `attachmentProductTags` | No | TypeScript, Android, iOS | Products tagged on individual image attachments. An `AmityAttachmentProductTags` container, keyed by each attachment's `fileId`. | **Product tags (SDK API).** An image post has a text parent and an image child. `productTags` tags products mentioned in the parent **text**. `attachmentProductTags` tags products on the child **media attachment**, and is not an array: it is an `AmityAttachmentProductTags` container that maps each attachment's `fileId` to that attachment's media tags. Flutter does not expose product tags on any post builder. See [Tag Products](#tag-products) below for both. UIKit's product-tag UI is a separate surface and may support fewer post types; see [UIKit Product Tagging](/uikit/components/social/product-tagging). For tagging limits and tap-through behavior, see [Product Tagging](/uikit/components/social/posts#product-tagging). ## 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(); } ``` ## Tag Products TypeScript, Android, and iOS accept two separate product inputs when creating an image post. The text tag is applied to the parent; the attachment tag is applied to the image child. | Input | Tags what | Shape | | --- | --- | --- | | `productTags` | Products named in the post text | An array of text product tags, each with a character range | | `attachmentProductTags` | Products shown in one image | An `AmityAttachmentProductTags` container, keyed by `fileId` | Build `attachmentProductTags` by setting the tags for each uploaded image's `fileId`. Images you do not set tags for stay untagged. ```swift iOS let postRepository = AmityPostRepository() let builder = AmityImagePostBuilder() builder.setImages(uploadedImages) builder.setText("Wearing the featured product today") let attachmentTags = AmityAttachmentProductTags() attachmentTags.set( fileId: uploadedImages[0].fileId, tags: [AmityMediaProductTag(productId: "prod_001")] ) let post = try await postRepository.createImagePost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil, productTags: [ AmityTextProductTag(productId: "prod_001", index: 12, length: 12) ], attachmentProductTags: attachmentTags ) ``` ```kotlin Android val attachmentTags = AmityAttachmentProductTags.Builder() attachmentTags.set( uploadedImages.first().getFileId(), listOf(AmityProductTag.Media(productId = "prod_001")) ) postRepository.createImagePost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, images = uploadedImages, text = "Wearing the featured product today", productTags = listOf( AmityProductTag.Text( productId = "prod_001", text = "featured product", index = 12, length = 16 ) ), attachmentProductTags = attachmentTags.build() ) .subscribe({ post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) }) ``` ```typescript TypeScript import { AmityAttachmentProductTags, PostRepository } from "@amityco/ts-sdk"; const attachmentTags = new AmityAttachmentProductTags(); attachmentTags.set(fileId, [{ productId: "prod_001" }]); const { data: post } = await PostRepository.createPost({ targetType: "community", targetId: communityId, data: { text: "Wearing the featured product today", }, attachments: [{ type: "image", fileId }], productTags: [ { productId: "prod_001", text: "featured product", index: 12, length: 16 }, ], attachmentProductTags: attachmentTags, }); ``` ## 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.plus 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 | Platforms | Description | | --- | --- | --- | --- | | `videos`, `uploadedVideos`, or `fileId` | Yes | TypeScript, iOS, Android, Flutter | Uploaded video objects or video file IDs, depending on SDK. | | `text` | No | TypeScript, iOS, Android, Flutter | Caption text shown with the video post. | | `targetType` | Yes | TypeScript, iOS, Android, Flutter | Feed target, usually `community` or `user`. | | `targetId` | Yes | TypeScript, iOS, Android, Flutter | Community ID or user ID for the target feed. | | `metadata` | No | TypeScript, iOS, Android, Flutter | Custom metadata stored with the post where supported. | | `mentionees` | No | TypeScript, iOS, Android, Flutter | User mention payload where supported by the platform builder. | | `productTags` | No | TypeScript, Android, iOS | Products tagged in the post text. Text product tag types: `Amity.TextProductTag`, `AmityTextProductTag`, `AmityProductTag.Text`. | | `attachmentProductTags` | No | TypeScript, Android, iOS | Products tagged on individual video attachments. An `AmityAttachmentProductTags` container, keyed by each attachment's `fileId`. | **Product tags (SDK API).** A video post has a text parent and a video child. `productTags` tags products mentioned in the parent **text**. `attachmentProductTags` tags products on the child **media attachment**, and is not an array: it is an `AmityAttachmentProductTags` container that maps each attachment's `fileId` to that attachment's media tags. Flutter does not expose product tags on any post builder. See [Tag Products](#tag-products) below for both. UIKit's product-tag UI is a separate surface and may support fewer post types; see [UIKit Product Tagging](/uikit/components/social/product-tagging). For tagging limits and tap-through behavior, see [Product Tagging](/uikit/components/social/posts#product-tagging). ## 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(); } ``` ## Tag Products TypeScript, Android, and iOS accept two separate product inputs when creating a video post. The text tag is applied to the parent; the attachment tag is applied to the video child. | Input | Tags what | Shape | | --- | --- | --- | | `productTags` | Products named in the post text | An array of text product tags, each with a character range | | `attachmentProductTags` | Products shown in one video | An `AmityAttachmentProductTags` container, keyed by `fileId` | Build `attachmentProductTags` by setting the tags for each uploaded video's `fileId`. Videos you do not set tags for stay untagged. ```swift iOS let postRepository = AmityPostRepository() let builder = AmityVideoPostBuilder() builder.setVideos(uploadedVideos) builder.setText("Wearing the featured product today") let attachmentTags = AmityAttachmentProductTags() attachmentTags.set( fileId: uploadedVideos[0].fileId, tags: [AmityMediaProductTag(productId: "prod_001")] ) let post = try await postRepository.createVideoPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil, productTags: [ AmityTextProductTag(productId: "prod_001", index: 12, length: 12) ], attachmentProductTags: attachmentTags ) ``` ```kotlin Android val attachmentTags = AmityAttachmentProductTags.Builder() attachmentTags.set( uploadedVideos.first().getFileId(), listOf(AmityProductTag.Media(productId = "prod_001")) ) postRepository.createVideoPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, videos = uploadedVideos, text = "Wearing the featured product today", productTags = listOf( AmityProductTag.Text( productId = "prod_001", text = "featured product", index = 12, length = 16 ) ), attachmentProductTags = attachmentTags.build() ) .subscribe({ post -> showSuccessMessage(post.getPostId()) }, { error -> handleGeneralError(error) }) ``` ```typescript TypeScript import { AmityAttachmentProductTags, PostRepository } from "@amityco/ts-sdk"; const attachmentTags = new AmityAttachmentProductTags(); attachmentTags.set(fileId, [{ productId: "prod_001" }]); const { data: post } = await PostRepository.createPost({ targetType: "community", targetId: communityId, data: { text: "Wearing the featured product today", }, attachments: [{ type: "video", fileId }], productTags: [ { productId: "prod_001", text: "featured product", index: 12, length: 16 }, ], attachmentProductTags: attachmentTags, }); ``` ## 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 | Platforms | Description | | --- | --- | --- | --- | | `audios`, `uploadedAudios`, or `fileId` | Yes | TypeScript, iOS, Android | Uploaded audio objects or audio file IDs, depending on SDK. | | `text` | No | TypeScript, iOS, Android | Caption text shown with the audio post. | | `targetType` | Yes | TypeScript, iOS, Android | Feed target, usually `community` or `user`. | | `targetId` | Yes | TypeScript, iOS, Android | Community ID or user ID for the target feed. | | `metadata` | No | TypeScript, iOS, Android | Custom metadata stored with the post where supported. | | `mentionees` | No | TypeScript, iOS, Android | User mention payload where supported by the platform builder. | | `productTags` | No | iOS | Products named in the parent post text. | | `attachmentProductTags` | No | TypeScript, iOS | Products shown on the child audio attachment. An `AmityAttachmentProductTags` container keyed by `fileId`. | **Product tags (SDK API).** An audio post has a text parent and an audio child. iOS accepts both inputs on `createAudioPost`; TypeScript accepts `attachmentProductTags` for the child only. The public Android and Flutter audio creators expose neither field, so this page does not promise a post-creation workaround. UIKit's product-tag UI is a separate surface and may support fewer post types; see [UIKit Product Tagging](/uikit/components/social/product-tagging). For tagging limits and tap-through behavior, see [Product Tagging](/uikit/components/social/posts#product-tagging). ## 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.plus 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 | Platforms | Description | | --- | --- | --- | --- | | `files`, `uploadedFiles`, or `fileId` | Yes | TypeScript, iOS, Android, Flutter | Uploaded file objects or file IDs, depending on SDK. | | `text` | No | TypeScript, iOS, Android, Flutter | Caption text shown with the file post. | | `targetType` | Yes | TypeScript, iOS, Android, Flutter | Feed target, usually `community` or `user`. | | `targetId` | Yes | TypeScript, iOS, Android, Flutter | Community ID or user ID for the target feed. | | `metadata` | No | TypeScript, iOS, Android, Flutter | Custom metadata stored with the post where supported. | | `mentionees` | No | TypeScript, iOS, Android, Flutter | User mention payload where supported by the platform builder. | | `productTags` | No | TypeScript, iOS | Products named in the parent post text. | | `attachmentProductTags` | No | TypeScript, iOS | Products shown on child file attachments. An `AmityAttachmentProductTags` container keyed by `fileId`. | ## 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("Featured product resources") let attachmentTags = AmityAttachmentProductTags() attachmentTags.set( fileId: uploadedFiles[0].fileId, tags: [AmityMediaProductTag(productId: "prod_001")] ) return try await postRepository.createFilePost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil, productTags: [ AmityTextProductTag(productId: "prod_001", index: 0, length: 12) ], attachmentProductTags: attachmentTags ) } ``` ```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 { AmityAttachmentProductTags, PostRepository } from "@amityco/ts-sdk"; const attachmentTags = new AmityAttachmentProductTags(); attachmentTags.set(fileId, [{ productId: "prod_001" }]); const { data: post } = await PostRepository.createPost({ targetType: "community", targetId: communityId, data: { text: "Featured product resources", }, attachments: [ { type: "file", fileId, }, ], productTags: [ { productId: "prod_001", text: "Featured product", index: 0, length: 16 }, ], attachmentProductTags: attachmentTags, }); ``` ```dart Flutter Future createFilePost(List uploadedFiles) { return AmitySocialClient.newPostRepository() .createPost() .targetCommunity(communityId) .file(uploadedFiles) .text('Resources for the team') .post(); } ``` **Product tags (SDK API).** A file post has a text parent and one child per file attachment. TypeScript's generic `createPost` and iOS `createFilePost` expose both text `productTags` for the parent and `attachmentProductTags` for the file children. The public Android and Flutter file creators expose neither field. UIKit's product-tag UI is a separate surface and may support fewer post types; see [UIKit Product Tagging](/uikit/components/social/product-tagging). ## 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.plus 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 | Platforms | Description | | --- | --- | --- | --- | | `pollId` | Yes | TypeScript, iOS, Android, Flutter | ID of an existing poll created before post creation. | | `text` | No | TypeScript, iOS, Android, Flutter | Feed caption or prompt shown with the poll post. | | `targetType` | Yes | TypeScript, iOS, Android, Flutter | Feed target, usually `community` or `user`. | | `targetId` | Yes | TypeScript, iOS, Android, Flutter | Community ID or user ID for the target feed. | | `metadata` | No | TypeScript, iOS, Android, Flutter | Custom metadata stored with the post where supported. | | `mentionees` | No | TypeScript, iOS, Android, Flutter | User mention payload where supported by the platform builder. | | `productTags` | No | TypeScript, iOS | Products named in the parent post text. Poll choices are not product-tag targets. | ## 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 for featured product") return try await postRepository.createPollPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil, productTags: [ AmityTextProductTag(productId: "prod_001", index: 9, length: 12) ] ) } ``` ```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 for featured product", pollId, }, productTags: [ { productId: "prod_001", text: "featured product", index: 9, length: 16 }, ], }); ``` ```dart Flutter final post = await AmitySocialClient.newPostRepository() .createPost() .targetCommunity(communityId) .poll(pollId) .text('Vote in this poll') .post(); ``` **Product tags (SDK API).** A poll post has a text parent and a poll child. TypeScript's generic `createPost({ dataType: "poll", ... })` and iOS `createPollPost` expose text `productTags` for the parent caption. The public Android and Flutter poll creators expose no product-tag field, and a poll child has no attachment-tag input. UIKit's product-tag UI is a separate surface and may support fewer post types; see [UIKit Product Tagging](/uikit/components/social/product-tagging). ## 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 | Platforms | Description | | --- | --- | --- | --- | | `clip` or `fileId` | Yes | TypeScript, iOS, Android | Uploaded clip data or clip file ID, depending on SDK | | `text` | No | TypeScript, iOS, Android | Caption text shown with the post | | `displayMode` | No | TypeScript, iOS, Android | Clip display mode where supported, such as `fill` or `fit` | | `isMuted` | No | TypeScript, iOS, Android | Whether playback should start muted where supported | | `targetType` | Yes | TypeScript, iOS, Android | Feed target, usually `community` or `user` | | `targetId` | Yes | TypeScript, iOS, Android | Community ID or user ID for the target feed | | `productTags` | No | iOS | Products named in the parent post text. | | `attachmentProductTags` | No | TypeScript, iOS | Products shown on the child clip attachment. An `AmityAttachmentProductTags` container keyed by `fileId`. | **Product tags (SDK API).** A clip post has a text parent and a clip child. iOS accepts both inputs on `createClipPost`; TypeScript accepts `attachmentProductTags` for the child only. The public Android and Flutter clip creators expose neither field, so this page does not promise a post-creation workaround. UIKit's product-tag UI is a separate surface and may support fewer post types; see [UIKit Product Tagging](/uikit/components/social/product-tagging). For tagging limits and tap-through behavior, see [Product Tagging](/uikit/components/social/posts#product-tagging). ## 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 | Platforms | Description | | --- | --- | --- | --- | | `attachments`, uploaded media sets, or file IDs | Yes | TypeScript, iOS, Android | Uploaded media items to combine in one post. Supported attachment types depend on SDK. | | `text` | No | TypeScript, iOS, Android | Caption text shown with the mixed media post. | | `targetType` | Yes | TypeScript, iOS, Android | Feed target, usually `community` or `user`. | | `targetId` | Yes | TypeScript, iOS, Android | Community ID or user ID for the target feed. | | `metadata` | No | TypeScript, iOS, Android | Custom metadata stored with the post where supported. | | `mentionees` | No | TypeScript, iOS, Android | User mention payload where supported by the platform builder. | | `productTags` | No | iOS | Products named in the parent post text. Text product tags. | | `attachmentProductTags` | Yes on TypeScript | TypeScript, iOS | Products shown on child attachments. An `AmityAttachmentProductTags` container keyed by `fileId`. | **Product tags (SDK API).** A mixed-media post has a text parent and one child per attachment. iOS accepts both inputs on `createMixedMediaPost`, both optional. TypeScript accepts `attachmentProductTags` for the children only, and its type declares the field as required: pass an empty `new AmityAttachmentProductTags()` when the post has no product tags. The public Android and Flutter mixed-media creators expose neither field, so this page does not promise a post-creation workaround. UIKit's product-tag UI is a separate surface and may support fewer post types; see [UIKit Product Tagging](/uikit/components/social/product-tagging). For tagging limits and tap-through behavior, see [Product Tagging](/uikit/components/social/posts#product-tagging). ## 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. Mixed-structure posts can still be **read** from Flutter: pass `includeMixedStructure: true` to [`semanticSearchPosts()`](/social-plus-sdk/social/discovery-engagement/search/intelligent-search-post). The limitation is on creation only. ## 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. A room post has a text parent and a child room post; product tagging is attached to that post child, not to the room entity itself. 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 | Platforms | Description | | --- | --- | --- | --- | | `roomId` | Yes | TypeScript, iOS, Android | ID of the room to attach to the post | | `text` | No | TypeScript, iOS, Android | Caption text shown on the parent post | | `title` | No | TypeScript, iOS, Android | Optional title stored in the room child post data | | `targetType` | Yes | TypeScript, iOS, Android | Feed target, usually `community` or `user` | | `targetId` | Yes | TypeScript, iOS, Android | Community ID or user ID for the target feed | | `metadata` | No | TypeScript, iOS, Android | Custom metadata stored with the post where supported | | `productTags` | No | TypeScript, iOS, Android | Media products available on the child room post; each tag carries `productId` only | | `pinnedProductId` | No | TypeScript, iOS, Android | Product featured on the child at creation. Must also appear in `productTags` | **Product tags (SDK API).** Room posts take **media** product tags, which carry a `productId` and nothing else. The positional fields `text`, `index`, and `length` belong to text product tags and do not apply here. `createRoomPost` returns the parent post; pinning, unpinning, and later tag updates target its **child** room post. These are SDK capabilities; UIKit's product-tag UI is a separate surface and may support fewer post types. See [UIKit Product Tagging](/uikit/components/social/product-tagging) for UI coverage and [Livestream Product Tagging](/social-plus-sdk/video-new/broadcasting/product-tagging) for the lifecycle operations. ## 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", }, productTags: [{ productId: "prod_001" }], pinnedProductId: "prod_001", }); ``` ```kotlin Android import com.amity.socialcloud.sdk.model.core.producttag.AmityProductTag fun createRoomPost(roomId: String, communityId: String) { postRepository.createRoomPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, roomId = roomId, text = "Join this live room", title = "Live room", productTags = listOf(AmityProductTag.Media(productId = "prod_001")), pinnedProductId = "prod_001" ) .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, productTags: [AmityMediaProductTag(productId: "prod_001")], pinnedProductId: "prod_001" ) } ``` 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. --- ### [Event Posts](https://learn.social.plus/social-plus-sdk/social/content-management/posts/creation/event-post) > Publish a scheduled event as a post so it renders as an event card in feeds and community timelines. An event post attaches an existing event to a post. The post carries the author's text; the event supplies the card that renders inside the feed. Tapping the card opens the event detail page instead of the post detail page, so the event stays the primary surface. Create the event first with the Event Repository. See [Create Event](/social-plus-sdk/social/events/create-event). The event post references the returned `eventId`. An event post points to an `eventId` returned by event creation. The reference is immutable — a post can never be repointed at a different event. Publish to a user feed or a community feed. Private-community events are locked to their own community server-side. ## Data Type Event posts use a dedicated content type: | Field | Value | | --- | --- | | `dataType` | `"event"` | | `structureType` | `"event"` | | `data.eventId` | Required — the event this post is about | | `data.title` | Optional — prefilled from the event's `title`, editable | | `data.text` | Optional — prefilled from the event's `description`, editable | Publishing an event post with both `title` and `text` blank is valid — the event card is the payload. ## Parameters | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | | `eventId` | Yes | TypeScript, iOS, Android | ID of an existing event. The post cannot be created without it. | | `title` | No | TypeScript, iOS, Android | Post title. Prefilled from `event.title` in composer, but any string (including empty) is accepted. Android: `String?`, dropped from the payload when blank. | | `text` | No | TypeScript, iOS, Android | Post body. Prefilled from `event.description` in composer, but any string (including empty) is accepted. Android: `String`, defaults to `""`. | | `targetType` | Yes | TypeScript, iOS, Android | Feed target, `community` or `user`. Constrained by the event's origin — see [Target Rules](#target-rules). | | `targetId` | Yes | TypeScript, iOS, Android | Community ID or user ID for the target feed. | | `metadata` | No | TypeScript, iOS, Android | Custom metadata stored with the post where supported. | | `mentionees` | No | iOS, TypeScript | User mention payload where supported by the platform builder. | | `mentionUserIds` | No | Android | `Set` of user IDs to mention. | | `hashtags` | No | Android | Hashtags stored with the post. | | `tags` | No | Android | Post tags. | The event-post SDK surface is available in TypeScript, iOS, and Android. Flutter and React Native do not expose these event-post APIs. ## Create an Event Post Creating an event post publishes the post and returns the created post once the server has accepted it. `eventId` is required; `title` and `text` are the author's own copy and may both be empty. ```swift iOS let postRepository = AmityPostRepository() // `eventId` (required, immutable) and `text` are init parameters; only `title` has a setter. let builder = AmityEventPostBuilder(eventId: eventId, text: "Come say hi") builder.setTitle("Summer Meetup") let post = try await postRepository.createEventPost( builder, targetId: communityId, targetType: .community, metadata: nil, mentionees: nil ) ``` ```kotlin Android postRepository.createEventPost( targetType = AmityPost.TargetType.COMMUNITY, targetId = communityId, eventId = eventId, title = "Summer Meetup", text = "Come say hi" ) .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: "event", data: { eventId, title: "Summer Meetup", text: "Come say hi", }, }); ``` ## Target Rules The target is constrained by the event's origin: | Event origin | Allowed targets | Enforcement | | --- | --- | --- | | Public community | The event's own community, any community the user can post in, or the user's own feed | Client picks; server accepts any valid target | | Private community | The event's own community only | Server-enforced — a `400` is returned for any other target | | User-hosted (`originType: "user"`) | Any community the user can post in, or the user's own feed | Client picks; server accepts any valid target | The private-community lock is a **server-side** rule. If your app builds a custom target picker, honour the same rule client-side to avoid surprising `400` errors after the user has already drafted a post. ## Rejected Combinations The backend rejects the following with `400`: | Case | Reason | | --- | --- | | Missing `data.eventId` | Required for `dataType: "event"`. | | Disallowed target | Private-community event posted anywhere other than that community. | | Media attachment | Image, video, file, audio, or clip children are not permitted — the event card is the visual. | | Product tags | `productTags` or `attachmentProductTags` are not permitted on event posts. | | Changing `data.eventId` | The event reference is immutable. Delete the post and create a new one to point at a different event. | ## Editing an Event Post Editing updates `data.title` and `data.text` only. The event itself is never changed by a post edit, and sibling posts referencing the same event are unaffected. ```swift iOS // `eventId` is required by the builder's init but is never sent on update (immutable). let builder = AmityEventPostBuilder(eventId: eventId, text: "Updated body") builder.setTitle("Updated title") try await postRepository.editPost(withId: postId, builder: builder) ``` ```kotlin Android postRepository.editPost(postId) .title("Updated title") .text("Updated body") .build() .apply() .subscribe() ``` ```typescript TypeScript await PostRepository.editPost(postId, { data: { title: "Updated title", text: "Updated body" }, }); ``` Do not send `data.eventId` on edit. It is immutable — any request that changes it is rejected with `400`. ## Resolve the Event from a Post There are two ways to resolve the event — pick one: a **snapshot** (`post.getEvent()`) that returns the event cached with the post, or a **live observer** (`EventRepository.getEvent`) that keeps the card in sync with changes. | Accessor | Role | Availability | | --- | --- | --- | | `post.getEvent()` | Synchronous snapshot from the sibling `events[]` collection cached with the post response. No network call, no subscription. Not a live observable. | ✅ **Shipped on iOS.** 🚧 Planned on other platforms. Android will surface it via the sealed `AmityPost.Data.EVENT` variant rather than a top-level method. | | `EventRepository.getEvent(eventId)` | Live observer. Subscribe while the card is on screen; unsubscribe on scroll-off or unmount. | ✅ Shipped on iOS, TypeScript, and Android. | On **iOS**, `post.getEvent()` is the recommended path — it's shipped, and the SDK links the event to the post during serialization, so it returns the event directly, with no repository call and without inspecting child posts yourself (this is what AmityUIKit does). On **Android**, resolve the event through the repository instead: read the `eventId` from the **child** post — the parent post's data is `TEXT` and the `EVENT` data sits on the child — then observe it with `EventRepository.getEvent(eventId)`. ```swift iOS // Shipped on iOS — the event is linked to the post during serialization. if let event = post.getEvent() { render(event) } ``` ```kotlin Android // The event reference lives on the child post, not the parent. val eventData = post.getChildren() .firstOrNull { it.getData() is AmityPost.Data.EVENT } ?.getData() as? AmityPost.Data.EVENT eventData?.let { data -> AmitySocialClient.newEventRepository() .getEvent(data.getEventId()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { event -> render(event) }, { /* event deleted or unavailable — render the unavailable card */ } ) } ``` ```typescript TypeScript import { EventRepository } from "@amityco/ts-sdk"; if (post.eventId) { const stopObserving = EventRepository.getEvent(post.eventId, ({ data: event }) => { if (event) render(event); }); // Call stopObserving when the event card leaves the screen. } ``` On iOS, `post.getEvent()` returns `nil` when the post is not an event post, or when the event entry is not in the payload (for example, the event was deleted). On Android, `getEvent(eventId)` is a live stream that emits again whenever the event changes. A deleted or unavailable event surfaces as an **error on the `Flowable`**, not a null emission — handle it in the error callback. A **cancelled** event still resolves normally and must be treated as deleted by the consumer; the SDK does not normalise it. ## Query Event Posts Event posts are **excluded from the default results** of `v3` and `v4` post queries so that older clients never receive a `dataType` they cannot render. Reads for event-aware clients must use **`GET /api/v5/posts`**. | Platform | Reading event posts | | --- | --- | | Android | `getPosts()` already targets `GET /api/v5/posts`, so event posts are returned without any extra filter. There is no `dataTypes` parameter to set. | | TypeScript | Pass `dataTypes: ["event"]` when reading through a `v3` / `v4` path. | To include event posts on any version, filter explicitly: ```typescript TypeScript import { PostRepository } from "@amityco/ts-sdk"; const stopObserving = PostRepository.getPosts( { targetType: "community", targetId: communityId, dataTypes: ["event"], }, ({ data }) => { renderResults(data); } ); ``` An unfiltered `v4` feed query will not return an event post you just created — the exclusion gate hides it. Either upgrade the read to `v5` or add `dataTypes=event` to the query. ## Related Topics Create the event that an event post will reference. Event model, statuses, and RSVP APIs. Post targets, structure types, and platform APIs. --- ### [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 | Platforms | Required | Description | | --- | --- | --- | --- | --- | | Query posts | Target filter | TypeScript, iOS, Android, Flutter | Context-dependent | Target type and target ID for user or community feeds. | | Query posts | Data type filter | TypeScript, iOS, Android, Flutter | No | Post content types to include, such as image, video, file, poll, or custom. | | Query posts | Deleted-state filter | TypeScript, iOS, Android, Flutter | No | Include or exclude deleted posts where supported. | | Query posts | Review/feed status | TypeScript, iOS, Android, Flutter | No | Published, reviewing, or declined state filters where exposed by the SDK. | | Query posts | `tags` | TypeScript, iOS, Android, Flutter | No | Tags to match when building tag-filtered feeds. | | Query posts | `includeMixedStructure` | TypeScript, iOS, Android; Flutter semantic search only | No | Include mixed-structure posts alongside media-type filters where supported. | | Query posts | `untilAt` | TypeScript, iOS, Android | No | Time boundary for pagination where supported. | | Android cache invalidation | `invalidateCache` | Android | 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 on `getPosts()`; available on [`semanticSearchPosts()`](/social-plus-sdk/social/discovery-engagement/search/intelligent-search-post) | | 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 | Platforms | Description | | --- | --- | --- | --- | --- | | Inspect post content | Post object | Yes | TypeScript, iOS, Android, Flutter | 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. With comment approval (iOS/TS), this is deprecated in favor of `getLocalCommentCount(includePending:)` — it equals `getLocalCommentCount(includePending: true)`; pass `includePending: false` for the shared, approved-only 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 | Platforms | Description | | --- | --- | --- | --- | --- | | Community pinned posts | `communityId` | Yes | All | Community whose pinned posts should be retrieved. | | Community pinned posts | `placement` | No | All | Placement such as default or announcement. | | Community pinned posts | `sortBy` | No | TypeScript, iOS | Sort order. Not accepted by the current Android call. Flutter exposes `sortByOptions`, but the request builder does not forward it — do not depend on it. | | Community pinned posts | `includeMixedStructure` | No | TypeScript, iOS, Android | Include mixed-structure posts. Not exposed on the current Flutter public method. | | Global pinned posts | `includeMixedStructure` | No | TypeScript, iOS, Android | Include mixed-structure posts in global pinned results. Not exposed on the current Flutter public method. | ## 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. ## Pin / Feature Controls Pinning, unpinning, featuring, and unfeaturing are performed from the **social.plus Console**, not from the client SDK. The client SDKs surface pinned and featured state as read-only. - **Event posts** in the UIKit follow the same rule and go a step further — the pinned / featured badge renders but there is no in-app pin control in any role, including hosts and moderators. See [Event Posts](../creation/event-post) and the [Event UIKit doc](/uikit/components/social/event) for how the badge renders. - **User-feed posts** can never be pinned regardless of role or Console state. - **Global featuring** requires a public community — a post in a private community cannot be featured globally. ## 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. Each comment also carries `rootId`, the ID of the top-level comment that starts the thread. `parentId` is the comment being replied to directly, while `rootId` stays the same for every comment in the thread — use it to group or navigate a whole thread without walking the parent chain. ## 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` | | Root comment | `comment.rootId` | `comment.rootId` | `comment.getRootId()` | `comment.rootId` | | 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 | | Approval status | `comment.approvalStatus` | `comment.approvalStatus` | `comment.getApprovalStatus()` | Not exposed | ## 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. A community can require a moderator to approve comments before anyone else sees them. In such a community, a created or edited comment comes back `pending` and stays out of public comment lists and counts until it is approved. See [Approval Lifecycle](./moderation/comment-review#approval-lifecycle). ## 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 | Platforms | Description | | --- | --- | --- | --- | | `referenceId` | Yes | TypeScript, iOS, Android, Flutter | Target content ID for the comment, such as a post ID. | | `referenceType` | Yes | TypeScript, iOS, Android, Flutter | Target content type, such as `post`. | | `text` | Yes | TypeScript, iOS, Android, Flutter | Text body for the comment or reply shown in these examples. | | `parentId` | No | TypeScript, iOS, Android, Flutter | Parent comment ID when creating a reply. | | `metadata` | No | TypeScript, iOS, Android, Flutter | App-defined metadata stored with the comment. Set through each platform's comment builder. | | `mentionees` / mention builders | No | TypeScript, iOS, Android, Flutter | Mention targets for text comments. Set through each platform's comment builder. | In a community that requires comment review, the created comment comes back with a `pending` approval status: only its author and the community's reviewers can see it, and it does not move the post's comment count for anyone else until it is approved. See [Approval Lifecycle](../moderation/comment-review#approval-lifecycle) for when a comment is held and who is exempt. In a community that requires review, create with optimistic creation turned off so the returned comment already carries its approval status — see [Read Approval Status on a Comment](../moderation/comment-review#read-approval-status-on-a-comment). ## 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 | Platforms | Description | | --- | --- | --- | --- | | `referenceId` | Yes | All | ID of the post, story, or custom content item being commented on | | `referenceType` | Yes | All | Use `post`, `story`, or `content` | | `text` | Yes | All | Text body of the comment | | `parentId` | No | All | Existing comment ID when creating a reply | | `metadata` | No | All | Custom metadata attached to the comment | | `mentionees` / mention builder | No | All | User mention payloads | | `links` | No | TypeScript, iOS, Android | Link metadata; not exposed on the Flutter comment builder | 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. Comment creation is local-first on TypeScript, iOS, Android, and React Native: the SDK persists a local comment before the server confirms it. See [Local-First Comment Creation](#local-first-comment-creation) for the per-platform sync states and failure handling. ```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; ``` ## Add Structured Links TypeScript, iOS, and Android accept a `links` payload when creating a comment, which stores the links found in the comment text alongside the comment. Comments highlight link text; they do not render link preview cards. Build each link with `renderPreview: false` and leave the preview metadata fields unset — a comment never needs a preview metadata lookup. Each link carries: | Field | Required | Description | | --- | --- | --- | | `url` | Yes | The link target. | | `renderPreview` | Yes | Set `false` for comments. Comments never render preview cards, so this value does not affect how a comment link displays. | | `index`, `length` | No | Position and length of the link text within the comment text. Include both whenever you send a `links` payload. | | `domain`, `title`, `imageUrl` | No | Preview card metadata. Leave unset for comments. | Sending `links` is optional. Clients detect URLs in the comment text and highlight them on their own, so a comment created without a `links` payload still renders highlighted links. When you do send the payload, include `index` and `length` for every link. On Android, `AmityLink` has no default arguments — pass `null` for every field you are not setting. ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const { data: comment } = await CommentRepository.createComment({ referenceId: postId, referenceType: "post", data: { text: "Check this out https://www.amity.co", }, links: [ { index: 15, length: 20, url: "https://www.amity.co", renderPreview: false, }, ], }); renderResults(comment); ``` ```swift iOS let options = AmityCommentCreateOptions( referenceId: "post-id", referenceType: .post, text: "Check this out https://www.amity.co", links: [ AmityLink( url: "https://www.amity.co", index: 15, length: 20, renderPreview: false ) ] ) let comment = try await commentRepository.createComment(with: options) showSuccessMessage(comment.commentId) ``` ```kotlin Android val links = listOf( AmityLink( index = 15, length = 20, url = "https://www.amity.co", renderPreview = false, domain = null, title = null, imageUrl = null ) ) AmitySocialClient.newCommentRepository() .createComment() .post(postId = postId) .with() .text(text = "Check this out https://www.amity.co") .links(links = links) .build() .send() .subscribe( { comment -> showSuccessMessage(comment.getCommentId()) }, { error -> handleGeneralError(error) } ) ``` When editing a comment on iOS, `links` follows update semantics: pass `nil` to leave the existing links unchanged, or `[]` to remove them all. See [Edit Comment](../actions/edit-comment). ## Local-First Comment Creation On TypeScript, iOS, Android, and React Native, the SDK writes the comment to its local store and starts the server request immediately, so an active comment collection can render it before the round trip completes. Flutter waits for the server response instead. The state is observable on every local-first platform, but the names and failure behavior differ: | Platform | State and failure behavior | | --- | --- | | TypeScript | Read `comment.syncState`: `syncing` while the request is in flight, `synced` after server confirmation, or `error` when creation fails. The optimistic item remains in the collection on failure and carries a temporary local comment ID. | | iOS | Observe `AmityComment.syncState`: `.syncing` becomes `.synced` after confirmation. A failed local comment becomes `.error` and is removed from its parent relationship, so it no longer appears among its parent's children. | | Android | Read `comment.getState()`: `SYNCING` becomes `SYNCED` after confirmation. A failed local comment is marked `FAILED` in the local store. | | Flutter | The current public comment-creation path waits for the server response before persisting the comment; it does not create the same local-first temporary comment. | | React Native | Uses `@amityco/ts-sdk-react-native`, which shares the TypeScript implementation and behaves identically. | Render local-first comments in a pending style, and handle each platform's failure state so a failed comment is not mistaken for a server-created one. On TypeScript and React Native, local-first creation requires the client cache. When the cache is disabled no local comment is created, and the call behaves like a plain server request. The creation call also rejects when the server request fails; it does not only set the failure state. Catch the error from the creation call and watch sync state through the live collection or observable query. Handling only one of the two leaves either an unhandled rejection or a pending comment that never resolves. ## 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 fetchedCommentId = 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. In a community that requires comment review, an edit that actually changes the comment, made by anyone who cannot review the community's comments, sends it back to `pending`: it leaves public comment lists and the post's comment count until a reviewer approves it again. An edit that changes nothing leaves the comment approved. Read `approvalStatus` on the returned comment instead of assuming the edit is live — see [How the Status Changes](../moderation/comment-review#how-the-status-changes) for which edits count and who is exempt. On a network where `isAllowEditCommentWhenReviewingEnabled` is `false`, editing a comment that is already pending fails with `403` / `400300`. ## Parameters | Operation | Parameter | Required | Platforms | Description | | --- | --- | --- | --- | --- | | Update text | `commentId` | Yes | TypeScript, iOS, Android, Flutter | Comment ID to update. | | Update text | `text` | Yes | TypeScript, iOS, Android, Flutter | Replacement text body for the comment. | | Update image attachments | `commentId` | Yes | TypeScript, iOS, Android, Flutter | Comment ID to update. | | Update image attachments | Uploaded image file ID | Yes | TypeScript, iOS, Android, Flutter | Uploaded image file ID to keep on the comment. | | Update optional fields | `metadata`, mentions | No | TypeScript, iOS, Android, Flutter | Optional editable fields. See [Update Fields](#update-fields) for each platform's builder method. | | Update optional fields | `links` | No | TypeScript, iOS, Android | Link attachments. Not exposed on the Flutter public update builder. | ## 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. Approve or decline comments held for moderator review. --- ### [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. --- ### [Comment Review](https://learn.social.plus/social-plus-sdk/social/content-management/comments/moderation/comment-review) > Hold new and edited comments for moderator review, then query the review queue and approve or decline them. Turn on comment review when a community should read a comment before anyone else does. A held comment still belongs to its author, who can keep editing it, but it stays out of comment lists and public comment counts until a reviewer approves it. Reviewers work from a community-wide queue and approve or decline by comment ID. ## Platform Support | Platform | Read approval status | Query review queue | Approve | Decline | | ---------- | ----------------------------- | ----------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------ | | TypeScript | `comment.approvalStatus` | `CommentRepository.getCommentReviewQueue(params, callback)` | `CommentRepository.approveComment(commentId)` | `CommentRepository.declineComment(commentId, reason?)` | | iOS | `comment.approvalStatus` | `commentRepository.getCommentReviewQueue(with:)` | `commentRepository.approveComment(withId:)` | `commentRepository.declineComment(withId:reason:)` | | Android | `comment.getApprovalStatus()` | `commentRepository.getCommentReviewQueue(communityId)` | `commentRepository.approveComment(commentId)` | `commentRepository.declineComment(commentId, reason)` | ## Approval Lifecycle Every comment carries an `approvalStatus`. This section is the single reference for when a comment is held, how its status moves, and what each move changes on the comment and its post. The method sections below show the calls. ### When Review Applies A comment is held for review only when all three of these are true: 1. It is written on a post that belongs to a community. This includes replies: a reply on such a post is held under the same rule as a top-level comment. 2. That community has `needApprovalOnCommentCreation` turned on. See [Require Review for Community Comments](#require-review-for-community-comments). 3. Its author cannot review that community's comments. The community's creator, and anyone who holds `REVIEW_COMMUNITY_COMMENT` on the community, publish immediately. Community moderators hold that permission by default. Every other comment is approved when it is written. | Comment on | Held for review | | --- | --- | | A post in a community with review on, written by a member who cannot review | Yes | | A post in a community with review on, written by the community's creator or a holder of `REVIEW_COMMUNITY_COMMENT` | No | | A post in a community with review off | No | | A post that is itself awaiting post review | Follows the same rule as any other post in its community | | A post on a user's own feed | No. There is no community to carry the setting. | | A story | No, even when the story's community has review on | | Your own content IDs (`referenceType: "content"`) | No | Changing the setting affects future transitions, not existing comments: - **Turning review on** changes no existing comment. A comment written before the switch enters review only if someone who cannot review later makes an effective edit to it. See [How the Status Changes](#how-the-status-changes). - **Turning review off** does not drain the queue. Comments already pending stay pending, stay in the review queue, and can still be approved or declined. ### Statuses | Status | TypeScript | iOS | Android | What it means | | -------- | ------------ | ----------- | ---------- | ------------- | | Pending | `"pending"` | `.pending` | `PENDING` | Held for review. Its author, the community's reviewers, and network admins can fetch it by ID; everyone else gets `404`. It appears in no comment query on TypeScript, iOS, or Android, not even its author's; reviewers find it in the [review queue](#query-the-review-queue). It accepts no replies, reactions, or flags. | | Approved | `"approved"` | `.approved` | `APPROVED` | Visible to everyone. Not terminal: an effective edit can send it back to pending. | | Rejected | `"rejected"` | `.rejected` | `REJECTED` | Declined by a reviewer. Terminal, and the comment is deleted at the same time. | The server omits `approvalStatus` on a comment that has never been through review, which includes every comment written before this feature. Every SDK reads an absent value as **approved**. Read the value from the SDK model, not the field's presence on the raw payload, and do not write your own defaulting. ### Comment Queries Show Approved Comments Only The TypeScript, iOS, and Android SDKs filter comment queries to approved comments, for every viewer, including the author of a pending comment. The same applies to a post's latest-comment previews and to reply lists. You do not need to filter pending comments out yourself, and you cannot get one back through a comment query. To show an author their own pending comment, keep the comment that create or edit returns and render it outside the list, for example with an "Awaiting review" label. Reviewers read pending comments through the [review queue](#query-the-review-queue). ### How the Status Changes ```mermaid stateDiagram-v2 direction LR [*] --> Approved: Review does not apply [*] --> Pending: Review applies Pending --> Approved: Approve Pending --> Rejected: Decline Pending --> Pending: Author edits Approved --> Pending: Effective edit Rejected --> [*] ``` | From | Trigger | Result | | --- | --- | --- | | New comment | Written where [review applies](#when-review-applies) | `pending` | | New comment | Written anywhere else, or by someone who can review | `approved` | | `pending` | A reviewer approves | `approved` | | `pending` | A reviewer declines | `rejected`, and the comment is deleted | | `pending` | The author edits | Stays `pending` and keeps its place in the queue. Refused with `403` / `400300` when the network's `isAllowEditCommentWhenReviewingEnabled` is `false`. | | `pending` | The author or a moderator deletes it | Deleted, and removed from the review queue | | `approved` | An effective edit by someone who cannot review, while the community has review on | `pending`. The comment leaves public comment lists and returns to the top of the review queue. | | `approved` | An edit that changes nothing, or an edit by someone who can review | Stays `approved` | | `approved` or `rejected` | Approve or decline again | Refused with `403` / `400300` | | `rejected` | Anything | Nothing. A rejected comment cannot be approved, edited, or restored. | An edit is **effective** when it changes the comment's text, or when it sends attachments, links, or metadata at all, even the same values as before. An edit that sends only unchanged text leaves the status as it is. ### What Approving or Declining Changes Only `approvalStatus` records the outcome on the comment. The other changes land on visibility, counts, and events. | What changes | Approve (`pending` → `approved`) | Decline (`pending` → `rejected`) | Re-review (`approved` → `pending`) | | --- | --- | --- | --- | | `approvalStatus` | `approved` | `rejected` | `pending` | | Who sees it | Everyone | No one: the comment is deleted | Author, reviewers, and network admins by ID only; it leaves every comment query | | `isDeleted` | Unchanged (`false`) | `true`, and its attachments are removed | Unchanged (`false`) | | Position in the thread | Where it was written, because `createdAt` does not change | Not shown | Keeps its `createdAt` | | `post.commentsCount` and the parent's reply count | Increase by one for everyone | Unchanged; a pending comment was never counted | Decrease by one until it is approved again | | Realtime event | `comment.approved` | A comment deletion. There is no `comment.declined` event. | A comment update carrying the `pending` status | | Notifications | Sent now, on the first approval only; none are sent while a comment is pending | None | None | The reviewer who decided, the time of the decision, and the decline `reason` are kept for the moderation record only. No SDK returns them. ## Permission Reading the queue and deciding on a comment both require the `REVIEW_COMMUNITY_COMMENT` permission on the target community. A client-side check is for hiding controls only; the server enforces the permission on every call, and a reviewer can never review their own comment. {/* doc-as-test: skip (comment review ships after the currently pinned Android SDK) */} ```kotlin Android import com.amity.socialcloud.sdk.model.core.permission.AmityPermission AmityCoreClient .hasPermission(permission = AmityPermission.REVIEW_COMMUNITY_COMMENT) .atCommunity(communityId = communityId) .check() .doOnNext { canReview: Boolean -> showSuccessMessage(canReview) } .subscribe() ``` {/* doc-as-test: skip (comment review ships after the currently pinned iOS SDK) */} ```swift iOS let canReview = await client.hasPermission( .reviewCommunityComment, forCommunity: "community-id" ) showSuccessMessage(canReview) ``` ```typescript TypeScript const canReview = client .hasPermission(Amity.Permission.ReviewCommunityCommentPermission) .community(communityId); renderResults(canReview); ``` ## Require Review for Community Comments Set `needApprovalOnCommentCreation` when you create or update the community. After it is on, new comments and effective edits in that community enter the queue, unless their author can review the community's comments. See [Approval Lifecycle](#approval-lifecycle) for the full rule. {/* doc-as-test: skip (comment review ships after the currently pinned Android SDK) */} ```kotlin Android AmitySocialClient.newCommunityRepository() .editCommunity(communityId = "community-id") .isCommentReviewEnabled(isCommentReviewEnabled = true) .build() .apply() .doOnSuccess { community: AmityCommunity -> renderResults(community.isCommentReviewEnabled()) } .subscribe() ``` {/* doc-as-test: skip (comment review ships after the currently pinned iOS SDK) */} ```swift iOS let updateOptions = AmityCommunityUpdateOptions() updateOptions.setNeedApprovalOnCommentCreation(true) let community = try await communityRepository.editCommunity( withId: "community-id", options: updateOptions ) renderResults(community.isCommentReviewEnabled) ``` ```typescript TypeScript import { CommunityRepository } from "@amityco/ts-sdk"; const { data: community } = await CommunityRepository.updateCommunity( communityId, { needApprovalOnCommentCreation: true, }, ); renderResults(community.needApprovalOnCommentCreation); ``` Turning the setting off does not drain the queue. Comments already waiting stay waiting, and the review queue keeps serving them until each one is decided. ## Read Approval Status on a Comment Read the status on any comment you hold — including the one returned by create and edit — before you show it as published. In a community that requires review, a create or an effective edit comes back pending. In a community that requires review, create comments with optimistic creation turned off. An optimistic create writes a local comment before the server answers, and the SDK cannot determine that comment's approval status yet; the correct status is reflected once the server responds. With optimistic creation off, nothing is written locally until the server confirms the comment, so the comment you receive already carries its approval status. | Platform | Turn optimistic creation off | | --- | --- | | Android | Call `.isOptimistic(false)` on the comment creator builder | | iOS | Pass `isOptimistic: false` to `AmityCommentCreateOptions` | | TypeScript | Pass `false` as the second argument: `createComment(bundle, false)` | {/* doc-as-test: skip (comment review ships after the currently pinned Android SDK) */} ```kotlin Android import com.amity.socialcloud.sdk.api.social.comment.review.AmityCommentApprovalStatus AmitySocialClient.newCommentRepository() .createComment() .post(postId = postId) .with() .text(text = "Hello world!") .isOptimistic(isOptimistic = false) // wait for the server's approval status .build() .send() .subscribe( { comment -> when (comment.getApprovalStatus()) { AmityCommentApprovalStatus.PENDING -> showSuccessMessage("Awaiting review — only you can see this comment") AmityCommentApprovalStatus.APPROVED -> showSuccessMessage(comment.getCommentId()) AmityCommentApprovalStatus.REJECTED -> showSuccessMessage("declined") } }, { error -> handleGeneralError(error) }, ) ``` {/* doc-as-test: skip (comment review ships after the currently pinned iOS SDK) */} ```swift iOS let options = AmityCommentCreateOptions( referenceId: "post-id", referenceType: .post, text: "Hello world!", isOptimistic: false // wait for the server's approval status ) let comment = try await commentRepository.createComment(with: options) switch comment.approvalStatus { case .pending: showSuccessMessage("Awaiting review — only you can see this comment") case .approved: showSuccessMessage(comment.commentId) case .rejected: showSuccessMessage("declined") } ``` ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const { data: comment } = await CommentRepository.createComment( { referenceId: postId, referenceType: "post", data: { text: "Hello world!" }, }, false, // isOptimistic: wait for the server's approval status ); if (comment.approvalStatus === "pending") { renderResults("Awaiting review — only you can see this comment"); } else { renderResults(comment); } ``` ## Query the Review Queue List a community's pending comments, newest first by when each entered the queue. A comment sent back for re-review returns to the top. | Input | Required | Description | | --- | --- | --- | | `communityId` | Yes | Community whose pending comments to list. | | `postId` | No | Narrows the queue to one post. A post from another community returns an empty list, not an error. | | `startDate` / `endDate` | No | Filter on when the comment was **written**, not when it entered the queue. `endDate` includes the whole calendar day. | | `timeZone` | No | IANA time-zone name the date filters are read in. Defaults to `UTC`. | | Page size | No | Defaults to **10** on this route, not the SDK-wide 20, and is capped at 100. | The queue is community-wide unless you narrow it to one post, and it stays readable after the community's review setting is turned off. {/* doc-as-test: skip (comment review ships after the currently pinned Android SDK) */} ```kotlin Android import org.joda.time.DateTime AmitySocialClient.newCommentRepository() .getCommentReviewQueue(communityId = communityId) .startDate(startDate = DateTime.parse("2026-09-01")) .endDate(endDate = DateTime.parse("2026-09-30")) .timeZone(timeZone = "Asia/Bangkok") .pageSize(pageSize = 10) .build() .query() .subscribe( { pagingData -> getPagingData(pagingData) }, { error -> handleGeneralError(error) } ) ``` {/* doc-as-test: skip (comment review ships after the currently pinned iOS SDK) */} ```swift iOS let options = AmityCommentReviewQueueQueryOptions( communityId: "community-id", postId: nil, startDate: nil, endDate: nil, timeZone: "Asia/Bangkok", pageSize: 10 ) token = commentRepository.getCommentReviewQueue(with: options).observe { collection, error in if let error = error { handleError(error) return } showSuccessMessage(collection.snapshots.count) } ``` ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const unsubscribe = CommentRepository.getCommentReviewQueue( { communityId, startDate: "2026-09-01", endDate: "2026-09-30", timeZone: "Asia/Bangkok", limit: 10, }, ({ data: comments, loading, error, onNextPage, hasNextPage }) => { if (loading) return; if (error) { handleError(error); return; } renderResults(comments); if (hasNextPage) { onNextPage?.(); } }, ); ``` ## Approve or Decline a Comment Approve to reveal the comment to everyone and move the post's comment count by one. Decline to reject it — a declined comment is deleted at the same time. [What Approving or Declining Changes](#what-approving-or-declining-changes) lists every effect of both. Both calls take the comment ID. Decline also takes an optional `reason`, at most 250 characters, kept for the moderation record only. Neither call is idempotent. A second decision on a comment that was already decided is refused, so two reviewers acting at once cannot both record an outcome. {/* doc-as-test: skip (comment review ships after the currently pinned Android SDK) */} ```kotlin Android AmitySocialClient.newCommentRepository() .approveComment(commentId = commentId) .subscribe( { showSuccessMessage(commentId) }, { error -> handleGeneralError(error) } ) AmitySocialClient.newCommentRepository() .declineComment(commentId = commentId, reason = "Off-topic") .subscribe( { showSuccessMessage(commentId) }, { error -> handleGeneralError(error) } ) ``` {/* doc-as-test: skip (comment review ships after the currently pinned iOS SDK) */} ```swift iOS try await commentRepository.approveComment(withId: "comment-id") try await commentRepository.declineComment(withId: "comment-id", reason: "Off-topic") showSuccessMessage() ``` ```typescript TypeScript import { CommentRepository } from "@amityco/ts-sdk"; const { data: approvedComment } = await CommentRepository.approveComment(commentId); const { data: declinedComment } = await CommentRepository.declineComment( commentId, "Off-topic", ); renderResults([approvedComment, declinedComment]); ``` ## Related SDK Functions Comment review changes how several other comment, post, and community APIs behave. The reference for each is on its own page; this table says only how review affects it. | Function | How comment review affects it | Reference | | --- | --- | --- | | Query comments (`getComments`) | Returns approved comments only, for every viewer including the author, on TypeScript, iOS, and Android. You do not filter pending comments out yourself. Show an author their own pending comment from the object create or edit returns. | [Query Comments](/social-plus-sdk/social/content-management/comments/retrieval/query-comments) | | Get a comment by ID | Its author, the community's reviewers, and network admins get a pending comment; everyone else gets `404`, which does not mean the comment was deleted. | [Get Comment](/social-plus-sdk/social/content-management/comments/retrieval/get-comment) | | Create a comment or reply | Comes back `pending` where review applies. In a community that requires review, create with optimistic creation off: an optimistic create cannot determine the comment's approval status until the server responds. A reply to a pending comment is refused. | [Create Comment](/social-plus-sdk/social/content-management/comments/creation/create-comment) · [Read Approval Status on a Comment](#read-approval-status-on-a-comment) | | Edit a comment | An effective edit sends an approved comment back to `pending`. Editing a pending comment fails when the network's `isAllowEditCommentWhenReviewingEnabled` social setting is `false`. | [Edit Comment](/social-plus-sdk/social/content-management/comments/actions/edit-comment) | | Delete a comment | The author or a moderator can delete a pending comment; it leaves the review queue. A decline also arrives as a deletion. | [Delete Comment](/social-plus-sdk/social/content-management/comments/actions/delete-comment) | | Add a reaction | Refused on a pending comment: `400` for its author, `404` for everyone else. | [Reactions](/social-plus-sdk/core-concepts/content-handling/reactions) | | Flag a comment | Refused on a pending comment. | [Flag a Comment](/social-plus-sdk/social/content-management/moderation/content-flagging#flag-a-comment) | | Post comment counts (`commentsCount`, `getLocalCommentCount`) | `post.commentsCount` counts approved comments only, for everyone. `getLocalCommentCount(includePending: true)` adds the current user's own pending comments. | [Live Comment Count](/social-plus-sdk/social/content-management/posts/overview#live-comment-count) | | Create or update a community | Carries the comment review setting: `needApprovalOnCommentCreation` on TypeScript, `setNeedApprovalOnCommentCreation(_:)` on iOS, and `isCommentReviewEnabled(_:)` on Android. | [Update Community](/social-plus-sdk/social/communities-spaces/community-lifecycle/update-community) · [Require Review](#require-review-for-community-comments) | | Check a permission (`hasPermission`) | `REVIEW_COMMUNITY_COMMENT` decides who can read the queue and decide, and whose own comments skip review. | [Roles and Permissions](/social-plus-sdk/core-concepts/user-management/roles-permissions) · [Permission](#permission) | | Subscribe to a realtime topic | Without a community or post comment subscription, approvals and declines do not reach the client. | [Social Realtime Events](/social-plus-sdk/core-concepts/realtime-communication/realtime-events/social-realtime-events) | ## Related Topics title="Post Review" href="/social-plus-sdk/social/content-management/posts/moderation/post-review" icon="gavel" > Approve, decline, and query posts held for community review. title="Edit Comment" href="/social-plus-sdk/social/content-management/comments/actions/edit-comment" icon="pen" > Update comment text and attachments, and see when an edit needs review again. title="Query Comments" href="/social-plus-sdk/social/content-management/comments/retrieval/query-comments" icon="filter" > List comments and replies on a post, story, or custom content ID. title="Social Realtime Events" href="/social-plus-sdk/core-concepts/realtime-communication/realtime-events/social-realtime-events" icon="bolt" > Subscribe to community and post topics so comment events reach the client. ## 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. Personalized For You feed, chronological global feed, custom-ranking global feed, and per-owner 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 | | --- | --- | | Give each user a personalized discovery tab | [For You Feed](./feed/for-you-feed) | | Build a real-time, newest-first feed | [Global Feed](./feed/global-feed) | | Surface globally what people engage with | [Custom-Ranking Global Feed](./feed/custom-ranking) | | 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 Compare the For You, global, and custom-ranking feeds. 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) > Choose between the personalized For You feed, the chronological global feed, and the custom-ranking global feed. Use `AmityFeedRepository` to read feed-style post collections. Each feed is a separate SDK entry point over the same content — what changes is who the posts are selected for, and how they are ordered. **Personalized per user.** Surfaces the posts each member is most likely to care about — from their communities, follows, public content, and live streams — so the home feed stays relevant instead of showing everything at once. Requires the network setting. **Chronological.** The most recent posts come first. Use it for a real-time content stream. **Score-sorted.** The same global content, ordered by engagement, recency, and updates rather than by time alone. **Scoped to one owner.** Query posts for a single user or community, with richer filters. ## Choosing a feed | If you need | Use | Ordering | | --- | --- | --- | | A discovery tab tailored to each user | [For You feed](/social-plus-sdk/social/discovery-engagement/feed/for-you-feed) | Backend-personalized and ranked | | A live stream of everything happening now | [Global feed](/social-plus-sdk/social/discovery-engagement/feed/global-feed) | Newest first | | A global feed that surfaces what people engage with | [Custom-ranking global feed](/social-plus-sdk/social/discovery-engagement/feed/custom-ranking) | Engagement score | | One user's or one community's posts | [Query posts](/social-plus-sdk/social/content-management/posts/retrieval/query-posts) | Newest first, with filters | For You feed is a network-level feature and must be enabled for your network before it returns content. The global and custom-ranking feeds are available to every network. ## Feed APIs | Feed | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | For You feed | `FeedRepository.getForYouFeed()` | Supported | Supported | Not available | | 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()` | | 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. ## 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. - Ranking is backend-owned on every feed that has it. Do not hardcode ranking assumptions in client UI logic. ## Related Topics Prebuilt newsfeed, global feed, and For You components. Search posts semantically or by hashtag. --- ### [Topic-based Content Discovery](https://learn.social.plus/social-plus-sdk/social/discovery-engagement/feed/curated-content) > Help users discover relevant community conversations through configured topics, using the SDK or the Discovery Widget. Topic-based content discovery helps brands and community teams bring relevant conversations to the pages people already visit. Instead of relying on users to search for a community, surface posts about a topic on your homepage, category pages, or other browsing surfaces. Post previews help people understand what your community offers and decide which conversations to explore. Your team configures a topic in the social.plus Console. The SDK uses its topic ID to fetch an ordered selection of community posts, called a **pool**. Build your own presentation with these posts, or use the UIKit's [Discovery Widget](/uikit/components/social/discovery-widget) for a ready-made carousel. The SDK supports topic-based content discovery on **TypeScript**, **iOS**, and **Android**. Flutter and React Native are not supported. ## How recommendations are selected Recommendations use three main signals: - **Topic relevance:** how closely a post matches the configured topic. This is the main signal. - **Engagement:** activity on a post, such as reactions and comments, helps identify relevant conversations people are participating in. - **Freshness:** newer posts help keep the selection timely. Selection is based on the configured topic rather than a reader's personal interests or engagement history. Topic configuration and eligibility rules determine which posts can appear. Viewer-specific restrictions, such as blocked authors, can further affect the returned selection. The SDK returns posts in recommendation order. Preserve that order when presenting the recommended selection. ## Before you start Configure a topic in the social.plus Console and obtain its topic ID. Initialize the SDK and establish a [user session](/social-plus-sdk/getting-started/authentication) or a [visitor session](/social-plus-sdk/getting-started/visitor-mode) before fetching posts. Topic configuration is managed in the Console. This page covers reading its recommendations and reporting interactions from your app. ## Parameters | Parameter | Type | Required | Platforms | Description | | --- | --- | --- | --- | --- | | `topicId` | `string` | Yes | TypeScript, iOS, Android | The topic ID configured in the Console. | | `limit` | integer | No | TypeScript, iOS, Android | Maximum number of posts to return, from 1 to 100. Omit to use the server default. | ## Fetch recommended posts `getPool` returns the topic and its ordered posts in a single request. It is not a live collection and does not support pagination. Call it again when your app needs a new selection. ```typescript TypeScript import { CuratedContentRepository } from '@amityco/ts-sdk'; const pool = await CuratedContentRepository.getPool('your-topic-id', 10); const topicName = pool.topic.topicName; const posts = pool.posts; ``` ```swift iOS let repository = AmityCuratedContentRepository() let pool = try await repository.getPool(topicId: "your-topic-id", limit: 10) let topicName = pool.topic.topicName let posts = pool.posts ``` ```kotlin Android val repository = AmityCoreClient.newCuratedContentRepository() repository.getPool(topicId = "your-topic-id", limit = 10) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ pool -> val topicName = pool.topic.topicName val posts = pool.posts }, { error -> // Handle the failed request in your app. }) ``` The returned posts are standard post objects that you can render in your own UI. Results may contain fewer posts than requested, including an empty list. An empty list is a successful response; omit the discovery surface when there is no content to display. ## Report interactions When building your own discovery surface, use the topic's analytics handle to measure what readers see and open. Report views when the surface or card becomes visible, rather than when an off-screen element is mounted. | Method | Report when | | --- | --- | | `markAsViewed()` | The discovery surface becomes visible. | | `markPostAsViewed(postId)` | A post card becomes visible. | | `markPostClick(postId)` | The reader selects a post card. | ```typescript TypeScript pool.topic.analytics.markAsViewed(); pool.topic.analytics.markPostAsViewed(post.postId); pool.topic.analytics.markPostClick(post.postId); ``` ```swift iOS pool.topic.analytics.markAsViewed() pool.topic.analytics.markPostAsViewed(postId: post.postId) pool.topic.analytics.markPostClick(postId: post.postId) ``` ```kotlin Android val topic = pool.topic topic.analytics().markAsViewed() topic.analytics().markPostAsViewed(post.getPostId()) topic.analytics().markPostClick(post.getPostId()) ``` Report the surface impression and each post impression once per page load. Report a click when the reader selects a card, and let navigation proceed without waiting for analytics. The Discovery Widget reports these interactions automatically, so you do not need to report them again for that component. ## Related topics Embed a ready-made carousel of topic-based recommendations. Establish a session for readers who have not signed in. --- ### [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. For Flutter, semantic post and community search are available from `amity_sdk` 7.8.7. ## Search Surfaces | Surface | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Semantic community search | `CommunityRepository.semanticSearchCommunities()` | `AmityCommunityRepository().semanticSearchCommunities(options:)` | `AmitySocialClient.newCommunityRepository().semanticSearchCommunities()` | `AmitySocialClient.newCommunityRepository().semanticSearchCommunities()` | | Semantic post search | `PostRepository.semanticSearchPosts()` | `AmityPostRepository().semanticSearchPosts(options:)` | `AmitySocialClient.newPostRepository().semanticSearchPosts()` | `AmitySocialClient.newPostRepository().semanticSearchPosts()` | | Hashtag post search | `PostRepository.searchPostsByHashtag()` | `AmityPostRepository().searchPostsByHashtag(options:)` | `AmitySocialClient.newPostRepository().searchPostsByHashtag()` | Not available | ## 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, Android, and Flutter community semantic search | Not applicable | | Target scope | Not applicable | Semantic post search can scope to a user or community target on TypeScript, iOS, Android, and Flutter | | Post data types | Not applicable | Supported for semantic post search on all platforms; hashtag search support differs by platform | | Mixed structure | Not applicable | Supported by semantic post search on TypeScript, iOS, Android, and Flutter; not exposed on Flutter's regular `getPosts()` builder | ## 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. - The SDK exposes semantic search for both posts and communities. Current Android, iOS, and Flutter UIKit global-search screens use semantic search for posts, while their community tabs use keyword search; use the SDK community repository when you need semantic community discovery in UIKit. --- ### [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 on TypeScript, iOS, Android, and Flutter, and hashtag post search for explicit hashtag matching on TypeScript, iOS, and Android. The current public Flutter SDK source does not expose hashtag post search. Semantic post search is available on Flutter. Flutter semantic post search requires `amity_sdk` 7.8.7 or later. ## Parameters | Parameter | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Query | `query` | `query` | `query` | `query` | | Target | `targetType`, `targetId` | `targetType`, `targetId` | `targetType`, `targetId` | `targetType`, `targetId` | | Data types | `dataTypes` | `dataTypes` | `postTypes` | `postTypes` | | Parent-only matching | `matchingOnlyParentPost` | `matchingOnlyParentPost` | `matchingOnlyParentPosts` | `matchingOnlyParentPosts` | | Mixed structure | `includeMixedStructure` | `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) } ) ``` ```dart Flutter final liveCollection = AmitySocialClient.newPostRepository() .semanticSearchPosts( query: 'healthy breakfast ideas', targetType: AmityPostTargetType.COMMUNITY, targetId: communityId, postTypes: const [AmityDataType.TEXT, AmityDataType.IMAGE], matchingOnlyParentPosts: true, includeMixedStructure: false, ) .getLiveCollection(pageSize: 20); liveCollection.getStreamController().stream.listen((posts) { // Render posts in your UI. }); await liveCollection.loadNext(); ``` ## Hashtag Search Filters | Parameter | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Hashtags | `hashtags` | `hashtags` | `hashtags` | Not available | | Target | `targetType` | Not exposed | Not exposed | Not available | | Data types | `dataTypes` | `dataTypes` | `dataTypes` | Not available | | Parent-only matching | `matchingOnlyParentPost` | `matchingOnlyParentPost` | Not exposed in the snippet surface | Not available | | Mixed structure | `includeMixedStructure` | `includeMixedStructure` | `includeMixedStructure` | Not available | ## 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. Hashtag search is not available in the Flutter SDK. - On Flutter, `getLiveCollection()` returns results in server relevance order; call `loadNext()` to page and `dispose()` when the screen is torn down. `getPagingData(token:, limit:)` is available for a one-shot query. - 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. Flutter semantic community search requires `amity_sdk` 7.8.7 or later. ## Parameters | Parameter | TypeScript | iOS | Android | Flutter | | --- | --- | --- | --- | --- | | Query | `query` | `query` | `query` | `query` | | Category IDs | `categoryIds` | `categoryIds` | `categoryIds` | `categoryIds` | | Tags | `tags` | `tags` | `AmityTags` | `tags` | | Membership status | `communityMembershipStatus` | `communityMembershipStatus` | `filter` | `filter` | | Discoverable private communities | `includeDiscoverablePrivateCommunity` | `includeDiscoverablePrivateCommunity` | `includeDiscoverablePrivateCommunity` | `includeDiscoverablePrivateCommunity` | ## 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) } ) ``` ```dart Flutter final liveCollection = AmitySocialClient.newCommunityRepository() .semanticSearchCommunities( query: 'trail running', filter: AmityCommunityFilter.ALL, tags: const ['outdoors'], categoryIds: const ['category-id'], includeDiscoverablePrivateCommunity: true, ) .getLiveCollection(pageSize: 20); liveCollection.getStreamController().stream.listen((communities) { // Render communities in your UI. }); await liveCollection.loadNext(); ``` ## Membership Status | Intent | iOS | Android | TypeScript | Flutter | | --- | --- | --- | --- | --- | | Search all visible communities | `.all` | `AmityCommunityMembershipStatusFilter.ALL` | Omit `communityMembershipStatus` for the default all-status search | `AmityCommunityFilter.ALL` (default) | | Search joined communities | `.member` | `AmityCommunityMembershipStatusFilter.MEMBER` | Use the SDK's typed membership-status value in app code | `AmityCommunityFilter.MEMBER` | | Search communities the user has not joined | `.notMember` | `AmityCommunityMembershipStatusFilter.NOT_MEMBER` | Use the SDK's typed membership-status value in app code | `AmityCommunityFilter.NOT_MEMBER` | ## 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. - Flutter defaults `includeDiscoverablePrivateCommunity` to `false`; set it to `true` to include discoverable private communities. - 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. - On Flutter, `includeDiscoverablePrivateCommunity` is available on `semanticSearchCommunities()` only. `getCommunities()` does not expose it. ## 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) } ) } ``` ## Data Freshness **The notification tray is not real-time.** Tray items are served from a short-lived server-side cache. A newly triggered notification is **not** guaranteed to appear the moment it happens — it becomes visible only after the server's cache refreshes. Until then, a query returns the previously cached result. Treat the tray as **eventually consistent**, not live, and do not rely on a new item (or an updated count) reflecting immediately after the triggering action. ## 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. **Not real-time.** Seen status is derived from the same server-cached tray data as [notification items](./notification-items#data-freshness), so it does not update the instant a new notification is triggered. Expect a short delay before `isSeen` / `lastTrayOccurredAt` reflect a new occurrence — an unread indicator built on this status is eventually consistent, not live. ## 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 | | `event` | Event-related notification (routes to the event detail page) | ## 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. - For `actionType == "event"` items, resolve the event id from `actionReferenceId` and route to the event detail page. When `trayItemCategory == "event_created"`, the hydrated `event` object (including `event.targetCommunity`) is available on the item for rendering the community avatar, event-type chip (`In-person` / `Virtual` from `event.type`), and event start date/time (from `event.startTime`). - 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. | | `isOriginPublic` | For a community-origin event, whether the origin community is public. User-origin events have no origin community; use `originType` when applying the event-post target rules — see [Target Rules](../content-management/posts/creation/event-post#target-rules). Android exposes it as `isOriginPublic()`. | | `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. Attach an event to a post so it renders as an event card in feeds. 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. | | `excludeOwnEvents` | Query | TypeScript, Android | Exclude events created by the current user — useful for discovery feeds. Defaults to `false`. | | 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. For discovery surfaces such as an **Explore** or **Happening now** feed, set `excludeOwnEvents: true` (TypeScript) or `.excludeOwnEvents(true)` (Android) to omit events created by the current user; it is a no-op when no authenticated user is resolved. ```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, excludeOwnEvents: true, // Exclude the current user's own events from the Explore, Happening, and Recommended surfaces }, ({ 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) .excludeOwnEvents(true) // For showing on happening event and recommended event .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` | | Exclude own events | `excludeOwnEvents` | — | `.excludeOwnEvents(...)` | 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