# Presence (/en/realtime-media/rtm/build/manage-presence-and-metadata/presence)

> For AI agents: see the complete documentation index at [llms.txt](/llms.txt).

<_PlatformTabsGroup groupMode="structured" canonicalPlatform="web" platforms="[&#x22;web&#x22;,&#x22;android&#x22;,&#x22;ios&#x22;,&#x22;macos&#x22;,&#x22;flutter&#x22;,&#x22;windows&#x22;,&#x22;linux-cpp&#x22;,&#x22;unity&#x22;]" showTabs="true">
  <_PlatformPanel platform="web">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="web" />

    In Signaling solutions, it is often important to know a user's current online status. For example, in instant messaging, chat applications, and online collaboration tools, users need to see the availability of their contacts. This information is typically displayed as a status message or icon next to a user's name. Presence features in Signaling SDK enable you to monitor join, leave, and status change notifications for users in a channel. Using Presence, you can:

    * Get a list of users currently in a channel and their temporary status data.
    * Get a list of channels a specified user has joined or is subscribed to.
    * Get, set, or remove user statuses.
    * Receive real-time event notifications when users join or leave specified channels.
    * Receive user status change event notifications in real time.

    ## Understand the tech [#understand-the-tech]

    Presence provides real-time information about the availability, and the current status of users, for effective communication and collaboration. It enables you to retrieve a list of users in a channel, or to query the list of channels for a specific user. The following figure illustrates how you integrate presence features into your app.

    **Presence workflow**

    ![Presence workflow](https://assets-docs.agora.io/images/signaling/presence-workflow.svg)

    ## Prerequisites [#prerequisites]

    Ensure that you have integrated the Signaling SDK in your project, and implemented the framework functionality from the [SDK quickstart](../../quickstart) page.

    <CalloutContainer type="info">
      <CalloutDescription>
        Presence features require Signaling SDK version 2.2.0 or later.
      </CalloutDescription>
    </CalloutContainer>

    ## Implement presence features [#implement-presence-features]

    Using presence, you can implement the following features:

    ### Get channel users [#get-channel-users]

    To obtain a list of online users in a channel, call `getOnlineUsers`. Depending on your parameter settings, this method returns a list of online user IDs in a channel and their temporary status data, or just the number of online users in the channel. You do not need to join a channel to call this method. This method is applicable to both message channels and stream channels. Use the `channelType` parameter to specify the channel type.

    After obtaining the initial online users list, update it in real time through `presence` event notifications.

    Refer to the following sample code to query the list of online users in a channel and their current status:

    ```javascript
    const options = {
        includedUserId: true,
        includedState: true
    };

    try {
        const result = await rtm.presence.getOnlineUsers(channelName, channelType, options);
        const { totalOccupancy, occupants, nextPage } = result;
        occupants.forEach((userInfo) => {
            const { states, userId, statesCount } = userInfo;
            console.log(`${userId} has states: ${states}`);
        });
    } catch (status) {
        const { operation, reason, errorCode } = status;
        console.log(`${operation} failed, ErrorCode: ${errorCode}, due to: ${reason}.`);
    }
    ```

    The `getOnlineUsers` method retrieves one page of data at a time. Each page contains up to 100 online users. If the channel has more than 100 users, the `nextPage` field in the returned `result` contains a bookmark for the next page. After each query, check if `result.nextPage` is not empty to determine if there is more data. To retrieve the next page, set the `page` field in `options` to the value of `result.nextPage`. Repeat this process until `result.nextPage` is empty. Refer to the following code:

    ```javascript
    const options = {
        includedUserId: true,
        includedState: true,
        page: "Next_Page_Bookmark"
    };
    try {
        const result = await rtm.presence.getOnlineUsers(channelName, channelType, options);
        // If nextPage exists, the value of nextPage should be filled into the page field of getOnlineUsers options for the next getOnlineUsers call.
        const { totalOccupancy, occupants, nextPage } = result;
        occupants.forEach((userInfo) => {
            const { states, userId, statesCount } = userInfo;
            console.log(`${userId} has states: ${states}`);
        });
    } catch (status) {
        const { operation, reason, errorCode } = status;
        console.log(`${operation} failed, ErrorCode: ${errorCode}, due to: ${reason}.`);
    }
    ```

    When there is a large number of users in a channel, you may only care about the total number of online users, and not their identities or temporary status. To get the total channel occupancy, set `includedState` and `includedUserId` in `options` to `false`.

    ```javascript
    const options = {
        includedUserId: false,
        includedState: false,
    };
    try {
        const result = await rtm.presence.getOnlineUsers(channelName, channelType, options);
        const { totalOccupancy, occupants, nextPage } = getOnlineUsersResult;
        occupants.forEach((userInfo) => {
            const { states, userId, statesCount } = userInfo;
            console.log(`${userId} has states: ${states}`);
        });
    } catch (status) {
        const { operation, reason, errorCode } = status;
        console.log(`${operation} failed, the error code is ${errorCode}, because of: ${reason}.`);
    }
    ```

    In this case, only the `totalOccupancy` property in the result is valid while all other fields are empty.

    <CalloutContainer type="info">
      <CalloutDescription>
        You cannot set `includedState` to `true` and `includedUserId` to `false` at the same time. To get the temporary status of users, you must also retrieve their user IDs.
      </CalloutDescription>
    </CalloutContainer>

    ### Get user channels [#get-user-channels]

    The `getUserChannels` method enables you to query which channels a user is currently in. This includes the message channels a user has subscribed to and the stream channels they have joined. This method is particularly useful for tracking user paths. Refer to the following sample code:

    ```javascript
    try {
        const getUserChannelsResult = await rtmClient.presence.getUserChannels(userId);
        const { channels, totalChannel } = getUserChannelsResult;
        channels.forEach((channelInfo) => {
            const { channelName, channelType } = channelInfo;
        });
    } catch (status) {
        const { operation, reason, errorCode } = status;
        console.log(`${operation} failed, the error code is ${errorCode}, because of: ${reason}.`);
    }
    ```

    The `getUserChannels` method returns complete query results about the channels and their types that the queried user is in, without pagination.

    ## User status management [#user-status-management]

    Signaling enables you to set and delete user status messages for the local user in each channel. The SDK notifies other online users in the channel of these changes through event notifications. This feature is useful in use-cases where user status sharing is required, such as real-time synchronization of the user's microphone status, mood, personal signature, score, and message input status.

    Signaling does not permanently save the status data. When a user unsubscribes from a channel, times out, or exits a channel, the data is deleted. To save user data permanently, use the [Store user metadata](storage/store-user-metadata) feature.

    When a user's temporary status changes, Signaling triggers a `REMOTE_STATE_CHANGED` event notification in real time. Users who set `withPresence = true` when joining the channel, receive the event notification.

    ### Set status [#set-status]

    Using presence, you can set the temporary user status for the local user. When you set the status before subscribing to or joining a channel, the data is cached on the client and does not take effect immediately. The status is updated and corresponding event notifications are triggered when you subscribe to or join a channel. The `setState` method applies to both message and stream channels; use the `channelType` parameter to specify the channel type.

    ```javascript
    const channelName = "test_channel";
    const channelType = "MESSAGE";
    const states = {
        "Mode": "Happy",
        "Mic": "False",
    };
    try {
        const result = await rtmClient.presence.setState(channelName, channelType, states);
    } catch (status) {
        const { operation, reason, errorCode } = status;
        console.log(`${operation} failed, ErrorCode: ${errorCode}, because of: ${reason}.`);
    }
    ```

    When using `setState` to set temporary user status, if the specified key already exists, its value is overwritten by the new value. If the specified key does not exist, a new key/value pair is added.

    ### Get status [#get-status]

    To obtain the temporary user status set by a user in a specified channel, use the `getState` method:

    ```javascript
    const channelName = "Chat_room";
    const channelType = "MESSAGE";
    const userId = "Tony";
    try {
        const result = await rtmClient.presence.getState(userId, channelName, channelType);
        const { states, userId, statesCount } = result; // Tony's temporary state
    } catch (status) {
        const { operation, reason, errorCode } = status;
        console.log(`${operation} failed, ErrorCode: ${errorCode}, because of: ${reason}.`);
    }
    ```

    Use the `getState` method to obtain the temporary status of other online users in the channel. If the queried user is not present in the specified channel, a `RTM_ERROR_PRESENCE_USER_NOT_EXIST` error message is returned by the SDK.

    ### Delete status [#delete-status]

    Each user can set up to 32 key/value pairs in a channel. To remove items that are no longer needed, call `removeState` with a list of keys. The `removeState` method only deletes temporary user status data for the local user.

    ```javascript
    const channelName = "Chat_room";
    const channelType = "MESSAGE";
    const removeKeys = ["Mode", "Mic"];
    const options = {
        states: removeKeys,
    }
    try {
        const result = await rtmClient.presence.removeState(channelName, channelType, options);
    } catch (status) {
        const { operation, reason, errorCode } = status;
        console.log(`${operation} failed, ErrorCode: ${errorCode}, because of: ${reason}.`);
    }
    ```

    Both the `setState` and `removeState` methods trigger `REMOTE_STATE_CHANGED` event notifications. Users who join the channel with `withPresence` set to `true` receive event notifications containing full data of the user's temporary status.

    ## Receive presence event notifications [#receive-presence-event-notifications]

    To receive presence event notifications, implement an event listener. See [event listeners](/en/api-reference/api-ref/signaling#event-listeners) for details. In addition, set the `withPresence` parameter to `true` when subscribing to or joining a channel.

    ### Event notification modes [#event-notification-modes]

    The presence event notification mode determines how subscribed users receive event notifications. There are two notification modes:

    * `Announce`: Real-time notification mode
    * `Interval`: Scheduled notification mode

    You set the **Max number of instant event** value in Agora Console to specify the condition for switching between the two modes. The scheduled notification mode helps prevent event noise that results from a large number of online users in the channel. See [Presence configuration](../../manage-agora-account#presence-configuration) for details.

    #### Real-time notification mode [#real-time-notification-mode]

    If the number of instant notifications in the channel is less than the **Max number of instant event** value, presence event notifications operate in real-time notification mode. In this mode, `REMOTE_JOIN`, `REMOTE_LEAVE`, `REMOTE_TIMEOUT`, and `REMOTE_STATE_CHANGED` notifications are sent to the client in real-time.

    <CodeBlockTabs defaultValue="Join">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Join">
          Join
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Leave">
          Leave
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Timeout">
          Timeout
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Snapshot">
          Snapshot
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="State change">
          State change
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Join">
        ```js
        {
              eventType: "REMOTE_JOIN";
              channelType: "MESSAGE";
              channelName: "test_channel";
              publisher: "publisher_name";
              stateChanged: {};
              interval: null;
              snapshot: null;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Leave">
        ```js
        {
              eventType: "REMOTE_LEAVE";
              channelType: "MESSAGE";
              channelName: "test_channel";
              publisher: "publisher_name";
              stateChanged: {};
              interval: null;
              snapshot: null;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Timeout">
        ```js
        {
              eventType: "REMOTE_TIMEOUT";
              channelType: "MESSAGE";
              channelName: "test_channel";
              publisher: "publisher_name";
              stateChanged: {};
              interval: null;
              snapshot: null;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Snapshot">
        ```js
        {
              eventType: "SNAPSHOT";
              channelType: "MESSAGE";
              channelName: "test_channel";
              publisher: "";
              stateChanged: {};
              interval: null;
              snapshot: [
                  { userId: "user_a", states: {}, statesCount: 0 },
                  { userId: "user_b", states: { key_1: "value_1" }, statesCount: 1 },
                  { userId: "yourSelf", states: {}, statesCount: 0 },
              ];
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="State change">
        ```js
        {
              eventType: "REMOTE_STATE_CHANGED";
              channelType: "MESSAGE";
              channelName: "test_channel";
              publisher: "publisher_name";
              stateChanged: {
                  "key_1": "value_1",
              };
              interval: null;
              snapshot: null;
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    #### Scheduled notification mode [#scheduled-notification-mode]

    When the number of online users in the channel exceeds the **Max number of instant event** value, the channel switches to the scheduled notification mode. In this mode, `REMOTE_JOIN`, `REMOTE_LEAVE`, `REMOTE_TIMEOUT`, and `REMOTE_STATE_CHANGED` events are replaced by `INTERVAL` events and sent to all users in the channel at specific time intervals. Users receive the following notification:

    ```js
    interval: {
        join: { users: ["Tony", "Lily"], userCount: 2 },
        leave: { users: ["Jason"], userCount: 1 },
        timeout: { users: ["Wang"], userCount: 1 },
        userStateList: [
            {
                userId: "Harvard",
                states: { "Mic": "False", "Position": "Beijing" },
                statesCount: 2,
            },
        ],
    }
    ```

    ## Reference [#reference]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### API reference [#api-reference]

    * [`getOnlineUsers`](/en/api-reference/api-ref/signaling#getonlineusers)
    * [`getUserChannels`](/en/api-reference/api-ref/signaling#getuserchannels)
    * [`setState`](/en/api-reference/api-ref/signaling#presencesetstatepropsag_platform)
    * [`getState`](/en/api-reference/api-ref/signaling#presencegetstatepropsag_platform)
    * [`removeState`](/en/api-reference/api-ref/signaling#presenceremovestatepropsag_platform)
    * [Event listeners](/en/api-reference/api-ref/signaling#event-listeners)

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="android">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="android" />

    In Signaling solutions, it is often important to know a user's current online status. For example, in instant messaging, chat applications, and online collaboration tools, users need to see the availability of their contacts. This information is typically displayed as a status message or icon next to a user's name. Presence features in Signaling SDK enable you to monitor join, leave, and status change notifications for users in a channel. Using Presence, you can:

    * Get a list of users currently in a channel and their temporary status data.
    * Get a list of channels a specified user has joined or is subscribed to.
    * Get, set, or remove user statuses.
    * Receive real-time event notifications when users join or leave specified channels.
    * Receive user status change event notifications in real time.

    ## Understand the tech [#understand-the-tech-1]

    Presence provides real-time information about the availability, and the current status of users, for effective communication and collaboration. It enables you to retrieve a list of users in a channel, or to query the list of channels for a specific user. The following figure illustrates how you integrate presence features into your app.

    **Presence workflow**

    ![Presence workflow](https://assets-docs.agora.io/images/signaling/presence-workflow.svg)

    ## Prerequisites [#prerequisites-1]

    Ensure that you have integrated the Signaling SDK in your project, and implemented the framework functionality from the [SDK quickstart](../../quickstart) page.

    <CalloutContainer type="info">
      <CalloutDescription>
        Presence features require Signaling SDK version 2.2.0 or later.
      </CalloutDescription>
    </CalloutContainer>

    ## Implement presence features [#implement-presence-features-1]

    Using presence, you can implement the following features:

    ### Get channel users [#get-channel-users-1]

    To obtain a list of online users in a channel, call `getOnlineUsers`. Depending on your parameter settings, this method returns a list of online user IDs in a channel and their temporary status data, or just the number of online users in the channel. You do not need to join a channel to call this method. This method is applicable to both message channels and stream channels. Use the `channelType` parameter to specify the channel type.

    After obtaining the initial online users list, update it in real time through `onPresenceEvent` event notifications.

    Refer to the following sample code to query the list of online users in a channel and their current status:

    <CodeBlockTabs defaultValue="Java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Java">
        ```java
        // Set the channelType to MESSAGE or STREAM
        RtmChannelType channelType = RtmChannelType.MESSAGE;
        PresenceOptions options = new PresenceOptions();
        options.setIncludeUserId(true);
        options.setIncludeState(true);

        mRtmClient.getPresence().getOnlineUsers(channelName, channelType, options, new ResultCallback<GetOnlineUsersResult>() {
            @Override
            public void onSuccess(GetOnlineUsersResult result) {
                log(CALLBACK, "getOnlineUsers success");
                for (UserState state : result.getUserStateList()) {
                    log(INFO, "user id: " + state.getUserId());
                    state.getStates().forEach((key, value) -> {
                        log(INFO, "key: " + key + ", value: " + value);
                    });
                }
            }

            @Override
            public void onFailure(ErrorInfo errorInfo) {
                log(ERROR, errorInfo.toString());
            }
        });
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Kotlin">
        ```kotlin
        // Set the channelType to MESSAGE or STREAM
        val channelType = RtmChannelType.MESSAGE
        val options = PresenceOptions().apply {
            setIncludeUserId(true)
            setIncludeState(true)
        }

        mRtmClient.getPresence().getOnlineUsers(channelName, channelType, options, object : ResultCallback<GetOnlineUsersResult> {
            override fun onSuccess(result: GetOnlineUsersResult) {
                log(CALLBACK, "getOnlineUsers success")
                for (state in result.userStateList) {
                    log(INFO, "user id: " + state.userId)
                    state.states.forEach { (key, value) ->
                        log(INFO, "key: $key, value: $value")
                    }
                }
            }

            override fun onFailure(errorInfo: ErrorInfo) {
                log(ERROR, errorInfo.toString())
            }
        })
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    The `getOnlineUsers` method retrieves one page of data at a time. Each page contains up to 100 online users. If the channel has more than 100 users, the `nextPage` field in the returned `result` contains a bookmark for the next page. After each query, check if `result.getNextPage` is empty to determine if there is more data. To retrieve the next page, set the `page` field in `PresenceOptions` to the value of `result.getNextPage`. Repeat this process until `result.getNextPage` is null. Refer to the following code:

    <CodeBlockTabs defaultValue="Java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Java">
        ```java
        PresenceOptions options =  new PresenceOptions();
        options.setIncludeUserId(true);
        options.setIncludeState(true);
        options.setPage("your_Next_Page_Bookmark");

        // Set the channelType to MESSAGE or STREAM
        RtmChannelType channelType = RtmChannelType.MESSAGE;
        mRtmClient.getPresence().getOnlineUsers(channelName, channelType, options, new ResultCallback<GetOnlineUsersResult>() {
            @Override
            public void onSuccess(GetOnlineUsersResult result) {
                // If nextPage exists, fill the value of nextPage into the page field of getOnlineUsersOptions for the next getOnlineUsers call
                log(CALLBACK, "getOnlineUsers success");
                log(INFO, "next page: " + result.getNextPage());
                for (UserState state : result.getUserStateList()) {
                    log(INFO, "user id: " + state.getUserId());
                    state.getStates().forEach((key, value) -> {
                        log(INFO, "key: " + key + ", value: " + value);
                    });
                }
            }

            @Override
            public void onFailure(ErrorInfo errorInfo) {
                log(ERROR, errorInfo.toString());
            }
        });
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Kotlin">
        ```kotlin
        val options = PresenceOptions().apply {
            setIncludeUserId(true)
            setIncludeState(true)
            setPage("your_Next_Page_Bookmark")
        }

        // Set the channelType to MESSAGE or STREAM
        val channelType = RtmChannelType.MESSAGE
        mRtmClient.getPresence().getOnlineUsers(channelName, channelType, options, object : ResultCallback<GetOnlineUsersResult> {
            override fun onSuccess(result: GetOnlineUsersResult) {
                // If nextPage exists, fill the value of nextPage into the page field of getOnlineUsersOptions for the next getOnlineUsers call
                log(CALLBACK, "getOnlineUsers success")
                log(INFO, "next page: $\{result.nextPage\}")
                for (state in result.userStateList) {
                    log(INFO, "user id: $\{state.userId\}")
                    state.states.forEach { (key, value) ->
                        log(INFO, "key: $key, value: $value")
                    }
                }
            }

            override fun onFailure(errorInfo: ErrorInfo) {
                log(ERROR, errorInfo.toString())
            }
        })
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    When there is a large number of users in a channel, you may only care about the total number of online users, and not their identities or temporary status. To get the total channel occupancy, set `includeState` and `includeUserId` in `PresenceOptions` to `false`.

    <CodeBlockTabs defaultValue="Java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Java">
        ```java
        PresenceOptions options = new PresenceOptions();
        options.setIncludeUserId(false);
        options.setIncludeState(false);

        // Set the channelType to MESSAGE or STREAM
        RtmChannelType channelType = RtmChannelType.MESSAGE;
        mRtmClient.getPresence().getOnlineUsers(channelName, channelType, options, new ResultCallback<GetOnlineUsersResult>() {
            @Override
            public void onSuccess(GetOnlineUsersResult result) {
                log(CALLBACK, "Total occupancy: " + result.getTotalOccupancy());
            }

            @Override
            public void onFailure(ErrorInfo errorInfo) {
                log(ERROR, errorInfo.toString());
            }
        });
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Kotlin">
        ```kotlin
        val options = PresenceOptions().apply {
            setIncludeUserId(false)
            setIncludeState(false)
        }

        // Set the channelType to MESSAGE or STREAM
        val channelType = RtmChannelType.MESSAGE
        mRtmClient.getPresence().getOnlineUsers(channelName, channelType, options, object : ResultCallback<GetOnlineUsersResult> {
            override fun onSuccess(result: GetOnlineUsersResult) {
                log(CALLBACK, "Total occupancy: $\{result.totalOccupancy\}")
            }

            override fun onFailure(errorInfo: ErrorInfo) {
                log(ERROR, errorInfo.toString())
            }
        })
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    In this case, only the `totalOccupancy` property in the result is valid while all other fields are empty.

    <CalloutContainer type="info">
      <CalloutDescription>
        You cannot set `includeState` to `true` and `includeUserId` to `false` at the same time. To get the temporary status of users, you must also retrieve their user IDs.
      </CalloutDescription>
    </CalloutContainer>

    ### Get user channels [#get-user-channels-1]

    The `getUserChannels` method enables you to query which channels a user is currently in. This includes the message channels a user has subscribed to and the stream channels they have joined. This method is particularly useful for tracking user paths. Refer to the following sample code:

    <CodeBlockTabs defaultValue="Java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Java">
        ```java
        mRtmClient.getPresence().getUserChannels("userid", new ResultCallback<ArrayList<ChannelInfo>>() {
            @Override
            public void onSuccess(ArrayList<ChannelInfo> channels) {
                log(CALLBACK, "get getUserChannels success");
                for (ChannelInfo channel : channels) {
                    log(INFO, channel.toString());
                }
            }

            @Override
            public void onFailure(ErrorInfo errorInfo) {
                log(ERROR, errorInfo.toString());
            }
        });
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Kotlin">
        ```kotlin
        mRtmClient.getPresence().getUserChannels("userid", object : ResultCallback<ArrayList<ChannelInfo>> {
            override fun onSuccess(channels: ArrayList<ChannelInfo>) {
                log(CALLBACK, "get getUserChannels success")
                for (channel in channels) {
                    log(INFO, channel.toString())
                }
            }

            override fun onFailure(errorInfo: ErrorInfo) {
                log(ERROR, errorInfo.toString())
            }
        })
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    The `getUserChannels` method returns complete query results about the channels and their types that the queried user is in, without pagination.

    ## User status management [#user-status-management-1]

    Signaling enables you to set and delete user status messages for the local user in each channel. The SDK notifies other online users in the channel of these changes through event notifications. This feature is useful in use-cases where user status sharing is required, such as real-time synchronization of the user's microphone status, mood, personal signature, score, and message input status.

    Signaling does not permanently save the status data. When a user unsubscribes from a channel, times out, or exits a channel, the data is deleted. To save user data permanently, use the [Store user metadata](storage/store-user-metadata) feature.

    When a user's temporary status changes, Signaling triggers a `REMOTE_STATE_CHANGED` event notification in real time. Users who set `withPresence = true` when joining the channel, receive the event notification.

    ### Set status [#set-status-1]

    Using presence, you can set the temporary user status for the local user. When you set the status before subscribing to or joining a channel, the data is cached on the client and does not take effect immediately. The status is updated and corresponding event notifications are triggered when you subscribe to or join a channel. The `setState` method applies to both message and stream channels; use the `channelType` parameter to specify the channel type.

    <CodeBlockTabs defaultValue="Java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Java">
        ```java
        HashMap<String, String> items = new HashMap<String, String>();
        items.put("Mode", "Happy");
        items.put("Mic", "False");

        mRtmClient.getPresence().setState("channel_name", RtmChannelType.MESSAGE, items, new ResultCallback<Void>() {
            @Override
            public void onSuccess(Void responseInfo) {
                log(CALLBACK, "setState success");
            }

            @Override
            public void onFailure(ErrorInfo errorInfo) {
                log(ERROR, errorInfo.toString());
            }
        });
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Kotlin">
        ```kotlin
        val items = HashMap<String, String>()
        items["Mode"] = "Happy"
        items["Mic"] = "False"

        mRtmClient.getPresence().setState("channel_name", RtmChannelType.MESSAGE, items, object : ResultCallback<Void> {
            override fun onSuccess(responseInfo: Void?) {
                log(CALLBACK, "setState success")
            }

            override fun onFailure(errorInfo: ErrorInfo) {
                log(ERROR, errorInfo.toString())
            }
        })
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    When using `setState` to set temporary user status, if the specified key already exists, its value is overwritten by the new value. If the specified key does not exist, a new key-value pair is added.

    ### Get status [#get-status-1]

    To obtain the temporary user status set by a user in a specified channel, use the `getState` method:

    <CodeBlockTabs defaultValue="Java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Java">
        ```java
        String userId = "Tony";
        mRtmClient.getPresence().getState("Chat_room", RtmChannelType.MESSAGE, userId, new ResultCallback<UserState>() {
            @Override
            public void onSuccess(UserState state) {
                log(CALLBACK, "get users(" + state.getUserId() + ") state success, " + state.toString());
            }

            @Override
            public void onFailure(ErrorInfo errorInfo) {
                log(ERROR, errorInfo.toString());
            }
        });
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Kotlin">
        ```kotlin
        val userId = "Tony"
        mRtmClient.getPresence().getState("Chat_room", RtmChannelType.MESSAGE, userId, object : ResultCallback<UserState> {
            override fun onSuccess(state: UserState) {
                log(CALLBACK, "get users($\{state.userId\}) state success, $\{state.toString()\}")
            }

            override fun onFailure(errorInfo: ErrorInfo) {
                log(ERROR, errorInfo.toString())
            }
        })
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    Use the `getState` method to obtain the temporary status of other online users in the channel. If the queried user is not present in the specified channel, a `PRESENCE_USER_NOT_EXIST` error message is returned by the SDK.

    ### Delete status [#delete-status-1]

    Each user can set up to 32 key-value pairs in a channel. To remove items that are no longer needed, call `removeState` with a list of keys. The `removeState` method only deletes temporary user status data for the local user.

    <CodeBlockTabs defaultValue="Java">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Java">
          Java
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Kotlin">
          Kotlin
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Java">
        ```java
        ArrayList<String> keys = new ArrayList&lt;&gt;(Arrays.asList("Mode", "Mic"));
        mRtmClient.getPresence().removeState("Chat_room", RtmChannelType.MESSAGE, keys, new ResultCallback<Void>() {
            @Override
            public void onSuccess(Void responseInfo) {
                log(CALLBACK, "removeState success");
            }

            @Override
            public void onFailure(ErrorInfo errorInfo) {
                log(ERROR, errorInfo.toString());
            }
        });
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Kotlin">
        ```kotlin
        val keys = ArrayList(listOf("Mode", "Mic"))
        mRtmClient.getPresence().removeState("Chat_room", RtmChannelType.MESSAGE, keys, object : ResultCallback<Void> {
            override fun onSuccess(responseInfo: Void?) {
                log(CALLBACK, "removeState success")
            }

            override fun onFailure(errorInfo: ErrorInfo) {
                log(ERROR, errorInfo.toString())
            }
        })
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    Both the `setState` and `removeState` methods trigger `REMOTE_STATE_CHANGED` event notifications. Users who join the channel with `withPresence` set to `true` receive event notifications containing full data of the user's temporary status.

    ## Receive presence event notifications [#receive-presence-event-notifications-1]

    A presence event notification returns the [PresenceEvent](/en/api-reference/api-ref/signaling#configpresenceeventpropsag_platform) data structure, which includes the [RtmPresenceEventType](/en/api-reference/api-ref/signaling#enumvpresencetypepropsag_platform) parameter.

    To receive presence event notifications, implement an event listener. See [event listeners](/en/api-reference/api-ref/signaling#event-listeners) for details. In addition, set the `withPresence` parameter to `true` when subscribing to or joining a channel.

    ### Event notification modes [#event-notification-modes-1]

    The presence event notification mode determines how subscribed users receive event notifications. There are two notification modes:

    * `Announce`: Real-time notification mode
    * `Interval`: Scheduled notification mode

    You set the **Max number of instant event** value in Agora Console to specify the condition for switching between the two modes. The scheduled notification mode helps prevent event noise that results from a large number of online users in the channel. See [Presence configuration](../../manage-agora-account#presence-configuration) for details.

    #### Real-time notification mode [#real-time-notification-mode-1]

    If the number of instant notifications in the channel is less than the **Max number of instant event** value, presence event notifications operate in real-time notification mode. In this mode, `REMOTE_JOIN`, `REMOTE_LEAVE`, `REMOTE_TIMEOUT`, and `REMOTE_STATE_CHANGED` notifications are sent to the client in real-time.

    <CodeBlockTabs defaultValue="Join">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Join">
          Join
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Leave">
          Leave
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Timeout">
          Timeout
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Snapshot">
          Snapshot
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="State change">
          State change
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Join">
        ```js
        {
              eventType: RtmPresenceEventType.REMOTE_JOIN;
              channelType: RtmChannelType.MESSAGE;
              channelName: "test_channel";
              publisher: "publisher_name";
              stateItems: {};
              interval: {
                  joinUserList:[],
                  leaveUserList:[],
                  timeoutUserList:[],
                  userStateList:[],
              };
              snapshot: {
                  userStateList:[]
              };
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Leave">
        ```js
        {
              eventType: RtmPresenceEventType.REMOTE_LEAVE;
              channelType: RtmChannelType.MESSAGE;
              channelName: "test_channel";
              publisher: "publisher_name";
              stateItems: {};
              interval: {
                  joinUserList:[],
                  leaveUserList:[],
                  timeoutUserList:[],
                  userStateList:[],
              };
              snapshot: {
                  userStateList:[]
              };
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Timeout">
        ```js
        {
              eventType: RtmPresenceEventType.REMOTE_TIMEOUT;
              channelType: RtmChannelType.MESSAGE;
              channelName: "test_channel";
              publisher: "publisher_name";
              stateItems: {};
              interval: {
                  joinUserList:[],
                  leaveUserList:[],
                  timeoutUserList:[],
                  userStateList:[],
              };
              snapshot: {
                  userStateList:[]
              };
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Snapshot">
        ```js
        {
              eventType: RtmPresenceEventType.SNAPSHOT;
              channelType: RtmChannelType.MESSAGE;
              channelName: "test_channel";
              publisher: "";
              stateItems: {};
              interval: {
                  joinUserList:[],
                  leaveUserList:[],
                  timeoutUserList:[],
                  userStateList:[],
              };
              snapshot: {
                  userStateList:[
                      {
                          userId: "user_a",
                          states: {}
                      },
                      {
                          userId: "user_b",
                          states: {
                              key: "key_1";
                              value: "value_1";
                          }
                      },
                      {
                          userId: "yourSelf",
                          states: {}
                      },
                  ]
              };
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="State change">
        ```js
        {
              eventType: RtmPresenceEventType.REMOTE_STATE_CHANGED;
              channelType: RtmChannelType.MESSAGE;
              channelName: "test_channel";
              publisher: "publisher_name";
              stateItems: {
                  "key_1":"value_1";
              };
              interval: {
                  joinUserList:[],
                  leaveUserList:[],
                  timeoutUserList:[],
                  userStateList:[],
              };
              snapshot: {
                  userStateList:[]
              };
             timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    #### Scheduled notification mode [#scheduled-notification-mode-1]

    When the number of online users in the channel exceeds the **Max number of instant event** value, the channel switches to the scheduled notification mode. In this mode, `REMOTE_JOIN`, `REMOTE_LEAVE`, `REMOTE_TIMEOUT`, and `REMOTE_STATE_CHANGED` events are replaced by `INTERVAL` events and sent to all users in the channel at specific time intervals. Users receive the following notification:

    ```js
    {
          eventType: RtmPresenceEventType.INTERVAL,
          channelTye: RtmChannelType.MESSAGE,
          channelName: "Chat_room",
          publisher: "",
          stateItems: {},
          interval: {
              joinUserList: ["Tony", "Lily"],
              leaveUserList: ["Jason"],
              timeoutUserList: ["Wang"],
              userStateList: [
                  {
                      userId: "Harvard",
                      states: [
                          {
                              key: "Mic",
                              value: "False"
                          },
                         {
                              key: "Position",
                              value: "Beijing"
                          }
                     ]
                  }
              ]
          },
          snapshot: {
              userStateList: []
          },
          timestamp: 1710487149497
    }
    ```

    ## Reference [#reference-1]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### API reference [#api-reference-1]

    * [`getOnlineUsers`](/en/api-reference/api-ref/signaling#getonlineusers)
    * [`getUserChannels`](/en/api-reference/api-ref/signaling#getuserchannels)
    * [`setState`](/en/api-reference/api-ref/signaling#presencesetstatepropsag_platform)
    * [`getState`](/en/api-reference/api-ref/signaling#presencegetstatepropsag_platform)
    * [`removeState`](/en/api-reference/api-ref/signaling#presenceremovestatepropsag_platform)
    * [Event listeners](/en/api-reference/api-ref/signaling#event-listeners)

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="ios">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="ios" />

    In Signaling solutions, it is often important to know a user's current online status. For example, in instant messaging, chat applications, and online collaboration tools, users need to see the availability of their contacts. This information is typically displayed as a status message or icon next to a user's name. Presence features in Signaling SDK enable you to monitor join, leave, and status change notifications for users in a channel. Using Presence, you can:

    * Get a list of users currently in a channel and their temporary status data.
    * Get a list of channels a specified user has joined or is subscribed to.
    * Get, set, or remove user statuses.
    * Receive real-time event notifications when users join or leave specified channels.
    * Receive user status change event notifications in real time.

    ## Understand the tech [#understand-the-tech-2]

    Presence provides real-time information about the availability, and the current status of users, for effective communication and collaboration. It enables you to retrieve a list of users in a channel, or to query the list of channels for a specific user. The following figure illustrates how you integrate presence features into your app.

    **Presence workflow**

    ![Presence workflow](https://assets-docs.agora.io/images/signaling/presence-workflow.svg)

    ## Prerequisites [#prerequisites-2]

    Ensure that you have integrated the Signaling SDK in your project, and implemented the framework functionality from the [SDK quickstart](../../quickstart) page.

    <CalloutContainer type="info">
      <CalloutDescription>
        Presence features require Signaling SDK version 2.2.0 or later.
      </CalloutDescription>
    </CalloutContainer>

    ## Implement presence features [#implement-presence-features-2]

    Using presence, you can implement the following features:

    ### Get channel users [#get-channel-users-2]

    To obtain a list of online users in a channel, call `getOnlineUsers`. Depending on your parameter settings, this method returns a list of online user IDs in a channel and their temporary status data, or just the number of online users in the channel. You do not need to join a channel to call this method. This method is applicable to both message channels and stream channels. Use the `channelType` parameter to specify the channel type.

    <CalloutContainer type="info">
      <CalloutDescription>
        After obtaining the initial online users list, update it in real time through `didReceivePresenceEvent` event notifications.
      </CalloutDescription>
    </CalloutContainer>

    Refer to the following sample code to query the list of online users in a channel and their current status:

    <Tabs defaultValue="swift" groupId="language">
      <TabsList>
        <TabsTrigger value="swift">
          Swift
        </TabsTrigger>

        <TabsTrigger value="objc">
          Objective-C
        </TabsTrigger>
      </TabsList>

      <TabsContent value="swift">
        ```swift
        let presenceOptions = AgoraRtmPresenceOptions()
        presenceOptions.includeState = true
        presenceOptions.includeUserId = true

        // Retrieve the user state of the current channel
        rtm.getPresence()?.getOnlineUsers(channelName: "your_channel", channelType: .message, options: presenceOptions) { response, errorInfo in
            if errorInfo == nil {
                print("getOnlineUsers success!!")
                let userCount = response?.totalOccupancy ?? 0
                let userStates = response?.userStateList ?? []

                // You can process userCount and userStates here
            } else {
                print("getOnlineUsers failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```
      </TabsContent>

      <TabsContent value="objc">
        ```objc
        AgoraRmPresenceOptions* presence_opt = [[AgoraRtmPresenceOptions alloc] init];
        presence_opt.includeState = true;
        presence_opt.includeUserId = true;
        [[rtm getPresence] getOnlineUsers:@"your_channel" channelType:AgoraRtmChannelTypeMessage options:presence_opt completion:^(AgoraRtmgetOnlineUsersResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"getOnlineUsers success!!");
                int user_count = response.totalOccupancy;
                NSArray<AgoraRtmUserState *> * user_states = response.userStateList;
            } else {
                NSLog(@"getOnlineUsers failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```

        The `getOnlineUsers` method retrieves one page of data at a time. Each page contains up to 100 online users. If the channel has more than 100 users, the `response.nextPage` field contains a bookmark for the next page. After each query, check `response.nextPage` to determine if there is more data. To retrieve the next page, set the `page` field in `AgoraRtmPresenceOptions` to the value of `response.nextPage`. Repeat this process until `response.nextPage` is empty.
      </TabsContent>
    </Tabs>

    <CodeBlockTabs defaultValue="Swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Objective-C">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Swift">
        ```swift
        let presenceOptions = AgoraRtmPresenceOptions()
        presenceOptions.includeState = true
        presenceOptions.includeUserId = true
        presenceOptions.page = "your_Next_Page_Bookmark"

        // Retrieve the user state of the current channel
        rtm.getPresence()?.getOnlineUsers("your_channel", channelType: .message, options: presenceOptions) { response, errorInfo in
            if errorInfo == nil {
                print("getOnlineUsers success!!")

                let userCount = response?.totalOccupancy ?? 0
                let nextPage = response?.nextPage ?? ""
                let userStates = response?.userStateList ?? []

                // You can process userCount, nextPage, and userStates here
            } else {
                print("getOnlineUsers failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Objective-C">
        ```objc
        AgoraRtmPresenceOptions* presence_opt = [[AgoraRtmPresence alloc] init];
        presence_opt.includeState = true;
        presence_opt.includeUserId = true;
        presence_opt.page = @"your_Next_Page_Bookmark";

        [[rtm getPresence] getOnlineUsers:@"your_channel" channelType:AgoraRtmChannelTypeMessage options:presence_opt completion:^(AgoraRtmgetOnlineUsersResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"getOnlineUsers success!!");
                int user_count = response.totalOccupancy;
                NSString* next_page = response.nextPage;
                NSArray<AgoraRtmUserState *> * user_states = response.userStateList;
            } else {
                NSLog(@"getOnlineUsers failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    When there is a large number of users in a channel, you may only care about the total number of online users, and not their identities or temporary status. To get the total channel occupancy, set `includeState` and `includeUserId` in `AgoraRtmPresenceOptions` to `false`.

    <CodeBlockTabs defaultValue="Swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Objective-C">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Swift">
        ```swift
        let presenceOptions = AgoraRtmPresenceOptions()
        presenceOptions.includeState = false
        presenceOptions.includeUserId = false

        // Retrieve the user state of the current channel
        rtm.getPresence()?.getOnlineUsers(channelName: "your_channel", channelType: .message, options: presenceOptions) { response, errorInfo in
            if errorInfo == nil {
                print("getOnlineUsers success!!")

                let userCount = response?.totalOccupancy ?? 0
            } else {
                print("getOnlineUsers failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Objective-C">
        ```objc
        AgoraRtmPresenceOptions* presence_opt = [[AgoraRtmPresence alloc] init];
        presence_opt.includeState = false;
        presence_opt.includeUserId = false;

        [[rtm getPresence] getOnlineUsers:@"your_channel" channelType:AgoraRtmChannelTypeMessage options:presence_opt completion:^(AgoraRtmgetOnlineUsersResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"getOnlineUsers success!!");
                int user_count = response.totalOccupancy;
            } else {
                NSLog(@"getOnlineUsers failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    In this case, only the `totalOccupancy` property in the result is valid while all other fields are empty.

    <CalloutContainer type="info">
      <CalloutDescription>
        You cannot set `includeState` to `true` and `includeUserId` to `false` at the same time. To get the temporary status of users, you must also retrieve their user IDs.
      </CalloutDescription>
    </CalloutContainer>

    ### Get user channels [#get-user-channels-2]

    The `getUserChannels` method enables you to query which channels a user is currently in. This includes the message channels a user has subscribed to and the stream channels they have joined. This method is particularly useful for tracking user paths. Refer to the following sample code:

    <CodeBlockTabs defaultValue="Swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Objective-C">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Swift">
        ```swift
        rtm.getPresence()?.getUserChannels("userId") { response, errorInfo in
            if errorInfo == nil {
                print("getUserChannels success!!")

                let channelCount = response?.totalChannel ?? 0
                let channels = response?.channels ?? []

            } else {
                print("getUserChannels failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Objective-C">
        ```objc
        [[rtm getPresence] getUserChannels:@"userId" completion:^(AgoraRtmGetUserChannelsResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"getUserChannels success!!");
                int channel_count = response.totalChannel;
                NSArray<AgoraRtmChannelInfo *> * channels = response.channels;
            } else {
                NSLog(@"getUserChannels failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    The `getUserChannels` method returns complete query results about the channels and their types that the queried user is in, without pagination.

    ## User status management [#user-status-management-2]

    Signaling enables you to set and delete user status messages for the local user in each channel. The SDK notifies other online users in the channel of these changes through event notifications. This feature is useful in use-cases where user status sharing is required, such as real-time synchronization of the user's microphone status, mood, personal signature, score, and message input status.

    Signaling does not permanently save the status data. When a user unsubscribes from a channel, times out, or exits a channel, the data is deleted. To save user data permanently, refer to [Store user metadata](storage/store-user-metadata).

    When a user's temporary status changes, Signaling triggers the remote state changed event notification in real-time. Users who enable presence in `features` when subscribing to a channel, receive the event notification.

    ### Set status [#set-status-2]

    Using presence, you can set the temporary user status for the local user. When you set the status before subscribing to or joining a channel, the data is cached on the client and does not take effect immediately. The status is updated and corresponding event notifications are triggered when you subscribe to or join a channel. The `setState` method applies to both message and stream channels; use the `channelType` parameter to specify the channel type.

    <CodeBlockTabs defaultValue="Swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Objective-C">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Swift">
        ```swift
        let states: [String: String] = ["key1": "value1", "key2": "value2"]

        rtm.getPresence()?.setState(channelName: "your_channel", channelType: .message, items: states) { response, errorInfo in
            if errorInfo == nil {
                print("setState success!!")
            } else {
                print("setState failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Objective-C">
        ```objc
        NSDictionary *states = @{@"key1": @"value1", @"key2": @"value2"};

        [[rtm getPresence] setState:@"your_channel" channelType:AgoraRtmChannelTypeMessage items:states completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"setState success!!");
            } else {
                NSLog(@"setState failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    When using `setState` to set temporary user status, if the specified key already exists, its value is overwritten by the new value. If the specified key does not exist, a new key-value pair is added.

    ### Get status [#get-status-2]

    To obtain the temporary user status set by a user in a specified channel, use the `getState` method:

    <Tabs defaultValue="swift" groupId="language">
      <TabsList>
        <TabsTrigger value="swift">
          Swift
        </TabsTrigger>

        <TabsTrigger value="objc">
          Objective-C
        </TabsTrigger>
      </TabsList>

      <TabsContent value="swift">
        ```swift
        rtm.getPresence()?.getState(channelName: "Chat_room", channelType: .message, userId: "userid") { response, errorInfo in
            if errorInfo == nil {
                print("getState success!!")

                if let state = response?.state {
                    // process state
                }
            } else {
                print("getState failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```

        If the queried user is not present in the specified channel, a `presenceUserNotExist` error message is returned by the SDK.
      </TabsContent>

      <TabsContent value="objc">
        ```objc
        [[rtm getPresence] getState:@"Chat_room" channelType:AgoraRtmChannelTypeMessage userId:@"userid" completion:^(AgoraRtmPresenceGetStateResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"getState success!!");
                AgoraRtmUserState* state = response.state;
            } else {
                NSLog(@"getState failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```

        Use the `getState` method to obtain the temporary status of other online users in the channel. If the queried user is not present in the specified channel, a `AgoraRtmErrorPresenceUserNotExist` error message is returned by the SDK.
      </TabsContent>
    </Tabs>

    ### Delete status [#delete-status-2]

    Each user can set up to 32 key-value pairs in a channel. To remove items that are no longer needed, call `removeState` with a list of keys. The `removeState` method only deletes temporary user status data for the local user.

    <Tabs defaultValue="swift" groupId="language">
      <TabsList>
        <TabsTrigger value="swift">
          Swift
        </TabsTrigger>

        <TabsTrigger value="objc">
          Objective-C
        </TabsTrigger>
      </TabsList>

      <TabsContent value="swift">
        ```swift
        let keys: [String] = ["key1", "key2"]

        rtm.getPresence()?.removeState("your_channel", channelType: .message, keys: keys) { response, errorInfo in
            if errorInfo == nil {
                print("removeState success!!")
            } else {
                print("removeState failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```

        Both the `setState` and `removeState` methods trigger `remoteStateChanged` event notifications. Users who join the channel with presence enabled in `features` receive event notifications containing full data of the user's temporary status.
      </TabsContent>

      <TabsContent value="objc">
        ```objc
        NSArray<NSString*>* keys = @[@"key1", @"key2"];
        [[rtm getPresence] removeState:@"your_channel" channelType:AgoraRtmChannelTypeMessage keys:keys completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"removeState success!!");
            } else {
                NSLog(@"removeState failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```

        Both the `setState` and `removeState` methods trigger `AgoraRtmPresenceEventTypeRemoteStateChanged` event notifications. Users who subscribe to the channel with presence enabled in `features` receive event notifications containing full data of the user's temporary status.
      </TabsContent>
    </Tabs>

    ## Receive presence event notifications [#receive-presence-event-notifications-2]

    A presence event notification returns the [AgoraRtmPresenceEvent](/en/api-reference/api-ref/signaling#configpresenceeventpropsag_platform) data structure, which includes the [AgoraRtmPresenceEventType](/en/api-reference/api-ref/signaling#enumvpresencetypepropsag_platform) parameter.

    To receive presence event notifications, implement an event listener. See [event listeners](/en/api-reference/api-ref/signaling#event-listeners) for details. In addition, include presence in the `features` parameter when subscribing to or joining a channel.

    ### Event notification modes [#event-notification-modes-2]

    The presence event notification mode determines how subscribed users receive event notifications. There are two notification modes:

    * `Announce`: Real-time notification mode
    * `Interval`: Scheduled notification mode

    Set the **Max number of instant event** value in Agora Console to specify the condition for switching between the two modes. The scheduled notification mode helps prevent event noise that results from a large number of online users in the channel. See [Presence configuration](../../manage-agora-account#presence-configuration) for details.

    #### Real-time notification mode [#real-time-notification-mode-2]

    If the number of instant notifications in the channel is less than the **Max number of instant event** value, presence event notifications operate in real-time notification mode. In this mode, the following notifications are sent to the client in real-time:

    **Swift**

    * `remoteJoinChannel`
    * `remoteLeaveChannel`
    * `remoteConnectionTimeout`
    * `remoteStateChanged`

    <CodeBlockTabs defaultValue="Join">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Join">
          Join
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Leave">
          Leave
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Timeout">
          Timeout
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Snapshot">
          Snapshot
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="State change">
          State change
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Join">
        ```js
        {
            type: .remoteJoinChannel;
            channelType: .message;
            channelName: "test_channel";
            publisher: "publisher_name";
            states: [];
            interval: [];
            snapshot: [];
            timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Leave">
        ```js
        {
            eventType: .remoteLeaveChannel;
            channelType: .message;
            channelName: "test_channel";
            publisher: "publisher_name";
            stateChanged: [];
            interval: [];
            snapshot: [];
            timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Timeout">
        ```js
        {
            eventType: .remoteConnectionTimeout;
            channelType: .message;
            channelName: "test_channel";
            publisher: "publisher_name";
            stateChanged: [];
            interval: [];
            snapshot: [];
            timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Snapshot">
        ```js
        {
            eventType: .snapshot;
            channelType: .message;
            channelName: "test_channel";
            publisher: "";
            stateChanged: [];
            interval: [];
            snapshot: [
                { userId: "user_a", states: {}},
                { userId: "user_b", states: { key_1: "value_1" }},
                { userId: "yourSelf", states: {}},
            ];
            timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="State change">
        ```js
        {
            eventType: .remoteStateChanged;
            channelType: .message;
            channelName: "test_channel";
            publisher: "publisher_name";
            states: {
                "key_1": "value_1",
            };
            interval: [];
            snapshot: [];
            timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    **Objective-C**

    * `AgoraRtmPresenceEventTypeRemoteJoinChannel`
    * `AgoraRtmPresenceEventTypeRemoteLeaveChannel`
    * `AgoraRtmPresenceEventTypeRemoteConnectionTimeout`
    * `AgoraRtmPresenceEventTypeRemoteStateChanged`

    <CodeBlockTabs defaultValue="Join">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Join">
          Join
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Leave">
          Leave
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Timeout">
          Timeout
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Snapshot">
          Snapshot
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="State change">
          State change
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Join">
        ```js
        {
              type: AgoraRtmPresenceEventTypeRemoteJoinChannel;
              channelType: AgoraRtmChannelTypeMessage;
              channelName: "test_channel";
              publisher: "publisher_name";
              states: [];
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Leave">
        ```js
        {
              eventType: AgoraRtmPresenceEventTypeRemoteLeaveChannel;
              channelType: AgoraRtmChannelTypeMessage;
              channelName: "test_channel";
              publisher: "publisher_name";
              stateChanged: [];
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Timeout">
        ```js
        {
              eventType: AgoraRtmPresenceEventTypeRemoteConnectionTimeout;
              channelType: AgoraRtmChannelTypeMessage;
              channelName: "test_channel";
              publisher: "publisher_name";
              stateChanged: [];
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Snapshot">
        ```js
        {
              eventType: AgoraRtmPresenceEventTypeSnapshot;
              channelType: AgoraRtmChannelTypeMessage;
              channelName: "test_channel";
              publisher: "";
              stateChanged: [];
              interval: [];
              snapshot: [
                  { userId: "user_a", states: {}},
                  { userId: "user_b", states: { key_1: "value_1" }},
                  { userId: "yourSelf", states: {}},
              ];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="State change">
        ```js
        {
              eventType: AgoraRtmPresenceEventTypeRemoteStateChanged;
              channelType: AgoraRtmChannelTypeMessage;
              channelName: "test_channel";
              publisher: "publisher_name";
              states: {
                  "key_1": "value_1",
              };
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    #### Scheduled notification mode [#scheduled-notification-mode-2]

    <Tabs defaultValue="swift" groupId="language">
      <TabsList>
        <TabsTrigger value="swift">
          Swift
        </TabsTrigger>

        <TabsTrigger value="objc">
          Objective-C
        </TabsTrigger>
      </TabsList>

      <TabsContent value="swift">
        When the number of online users in a channel exceeds the **Max number of instant event** value, the channel switches to the scheduled notification mode. In this mode, `remoteJoinChannel`, `remoteLeaveChannel`, `remoteConnectionTimeout`, and `remoteStateChanged` events are replaced by `interval` events and sent to all users in the channel at specific time intervals. Users receive the following notification:

        ```js
        {
              "type": "AgoraRtmPresenceEventTypeInterval",
              "channelType": "AgoraRtmChannelTypeMessages",
              "channelName": "Chat_room",
              "publisher": "Tony",
              "interval": {
                  "remote_join": ["Tony", "Lily"],
                  "remote_leave": ["Jason"],
                  "remote_timeout": ["Wendy"],
                  "remote_state_change": [
                      {
                          "userId": "Harvard",
                          "states": [
                              { "Mic": "False" },
                              { "Position": "Washington" }
                          ]
                      },
                      {
                          "userId": "Harvard",
                          "states": [
                              { "Mic": "True" },
                              { "Position": "Washington" }
                          ]
                      }
                  ]
              }
        }
        ```
      </TabsContent>

      <TabsContent value="objc">
        When the number of online users in the channel exceeds the **Max number of instant event** value, the channel switches to the scheduled notification mode. In this mode, `AgoraRtmPresenceEventTypeRemoteJoinChannel`, `AgoraRtmPresenceEventTypeRemoteLeaveChannel`, `AgoraRtmPresenceEventTypeRemoteConnectionTimeout`, and `AgoraRtmPresenceEventTypeRemoteStateChanged` events are replaced by `AgoraRtmPresenceEventTypeInterval` events and sent to all users in the channel at specific time intervals. Users receive the following notification:

        ```js
        {
              "type": "AgoraRtmPresenceEventTypeInterval",
              "channelType": "AgoraRtmChannelTypeMessages",
              "channelName": "Chat_room",
              "publisher": "Tony",
              "interval": {
                  "remote_join": ["Tony", "Lily"],
                  "remote_leave": ["Jason"],
                  "remote_timeout": ["Wendy"],
                  "remote_state_change": [
                      {
                          "userId": "Harvard",
                          "states": [
                              { "Mic": "False" },
                              { "Position": "Washington" }
                          ]
                      },
                      {
                          "userId": "Harvard",
                          "states": [
                              { "Mic": "True" },
                              { "Position": "Washington" }
                          ]
                      }
                  ]
              }
        }
        ```
      </TabsContent>
    </Tabs>

    ## Reference [#reference-2]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### API reference [#api-reference-2]

    <Tabs defaultValue="swift" groupId="language">
      <TabsList>
        <TabsTrigger value="swift">
          Swift
        </TabsTrigger>

        <TabsTrigger value="objc">
          Objective-C
        </TabsTrigger>
      </TabsList>

      <TabsContent value="swift">
        * [`getOnlineUsers`](/en/api-reference/api-ref/signaling?platform=ios\&tab=swift#getonlineusers)
        * [`getUserChannels`](/en/api-reference/api-ref/signaling?platform=ios\&tab=swift#getuserchannels)
        * [`setState`](/en/api-reference/api-ref/signaling?platform=ios\&tab=swift#presencesetstatepropsag_platform)
        * [`getState`](/en/api-reference/api-ref/signaling?platform=ios\&tab=swift#presencegetstatepropsag_platform)
        * [`removeState`](/en/api-reference/api-ref/signaling?platform=ios\&tab=swift#presenceremovestatepropsag_platform)
        * [Event listeners](/en/api-reference/api-ref/signaling?platform=ios\&tab=swift#event-listeners)
      </TabsContent>

      <TabsContent value="objc">
        * [`getOnlineUsers`](/en/api-reference/api-ref/signaling?platform=ios\&tab=objc#getonlineusers)
        * [`getUserChannels`](/en/api-reference/api-ref/signaling?platform=ios\&tab=objc#getuserchannels)
        * [`setState`](/en/api-reference/api-ref/signaling?platform=ios\&tab=objc#presencesetstatepropsag_platform)
        * [`getState`](/en/api-reference/api-ref/signaling?platform=ios\&tab=objc#presencegetstatepropsag_platform)
        * [`removeState`](/en/api-reference/api-ref/signaling?platform=ios\&tab=objc#presenceremovestatepropsag_platform)
        * [Event listeners](/en/api-reference/api-ref/signaling?platform=ios\&tab=objc#event-listeners)
      </TabsContent>
    </Tabs>

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="macos">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="macos" />

    In Signaling solutions, it is often important to know a user's current online status. For example, in instant messaging, chat applications, and online collaboration tools, users need to see the availability of their contacts. This information is typically displayed as a status message or icon next to a user's name. Presence features in Signaling SDK enable you to monitor join, leave, and status change notifications for users in a channel. Using Presence, you can:

    * Get a list of users currently in a channel and their temporary status data.
    * Get a list of channels a specified user has joined or is subscribed to.
    * Get, set, or remove user statuses.
    * Receive real-time event notifications when users join or leave specified channels.
    * Receive user status change event notifications in real time.

    ## Understand the tech [#understand-the-tech-3]

    Presence provides real-time information about the availability, and the current status of users, for effective communication and collaboration. It enables you to retrieve a list of users in a channel, or to query the list of channels for a specific user. The following figure illustrates how you integrate presence features into your app.

    **Presence workflow**

    ![Presence workflow](https://assets-docs.agora.io/images/signaling/presence-workflow.svg)

    ## Prerequisites [#prerequisites-3]

    Ensure that you have integrated the Signaling SDK in your project, and implemented the framework functionality from the [SDK quickstart](../../quickstart) page.

    <CalloutContainer type="info">
      <CalloutDescription>
        Presence features require Signaling SDK version 2.2.0 or later.
      </CalloutDescription>
    </CalloutContainer>

    ## Implement presence features [#implement-presence-features-3]

    Using presence, you can implement the following features:

    ### Get channel users [#get-channel-users-3]

    To obtain a list of online users in a channel, call `getOnlineUsers`. Depending on your parameter settings, this method returns a list of online user IDs in a channel and their temporary status data, or just the number of online users in the channel. You do not need to join a channel to call this method. This method is applicable to both message channels and stream channels. Use the `channelType` parameter to specify the channel type.

    <CalloutContainer type="info">
      <CalloutDescription>
        After obtaining the initial online users list, update it in real time through `didReceivePresenceEvent` event notifications.
      </CalloutDescription>
    </CalloutContainer>

    Refer to the following sample code to query the list of online users in a channel and their current status:

    <Tabs defaultValue="swift" groupId="language">
      <TabsList>
        <TabsTrigger value="swift">
          Swift
        </TabsTrigger>

        <TabsTrigger value="objc">
          Objective-C
        </TabsTrigger>
      </TabsList>

      <TabsContent value="swift">
        ```swift
        let presenceOptions = AgoraRtmPresenceOptions()
        presenceOptions.includeState = true
        presenceOptions.includeUserId = true

        // Retrieve the user state of the current channel
        rtm.getPresence()?.getOnlineUsers(channelName: "your_channel", channelType: .message, options: presenceOptions) { response, errorInfo in
            if errorInfo == nil {
                print("getOnlineUsers success!!")
                let userCount = response?.totalOccupancy ?? 0
                let userStates = response?.userStateList ?? []

                // You can process userCount and userStates here
            } else {
                print("getOnlineUsers failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```
      </TabsContent>

      <TabsContent value="objc">
        ```objc
        AgoraRmPresenceOptions* presence_opt = [[AgoraRtmPresenceOptions alloc] init];
        presence_opt.includeState = true;
        presence_opt.includeUserId = true;
        [[rtm getPresence] getOnlineUsers:@"your_channel" channelType:AgoraRtmChannelTypeMessage options:presence_opt completion:^(AgoraRtmgetOnlineUsersResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"getOnlineUsers success!!");
                int user_count = response.totalOccupancy;
                NSArray<AgoraRtmUserState *> * user_states = response.userStateList;
            } else {
                NSLog(@"getOnlineUsers failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```

        The `getOnlineUsers` method retrieves one page of data at a time. Each page contains up to 100 online users. If the channel has more than 100 users, the `response.nextPage` field contains a bookmark for the next page. After each query, check `response.nextPage` to determine if there is more data. To retrieve the next page, set the `page` field in `AgoraRtmPresenceOptions` to the value of `response.nextPage`. Repeat this process until `response.nextPage` is empty.
      </TabsContent>
    </Tabs>

    <CodeBlockTabs defaultValue="Swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Objective-C">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Swift">
        ```swift
        let presenceOptions = AgoraRtmPresenceOptions()
        presenceOptions.includeState = true
        presenceOptions.includeUserId = true
        presenceOptions.page = "your_Next_Page_Bookmark"

        // Retrieve the user state of the current channel
        rtm.getPresence()?.getOnlineUsers("your_channel", channelType: .message, options: presenceOptions) { response, errorInfo in
            if errorInfo == nil {
                print("getOnlineUsers success!!")

                let userCount = response?.totalOccupancy ?? 0
                let nextPage = response?.nextPage ?? ""
                let userStates = response?.userStateList ?? []

                // You can process userCount, nextPage, and userStates here
            } else {
                print("getOnlineUsers failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Objective-C">
        ```objc
        AgoraRtmPresenceOptions* presence_opt = [[AgoraRtmPresence alloc] init];
        presence_opt.includeState = true;
        presence_opt.includeUserId = true;
        presence_opt.page = @"your_Next_Page_Bookmark";

        [[rtm getPresence] getOnlineUsers:@"your_channel" channelType:AgoraRtmChannelTypeMessage options:presence_opt completion:^(AgoraRtmgetOnlineUsersResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"getOnlineUsers success!!");
                int user_count = response.totalOccupancy;
                NSString* next_page = response.nextPage;
                NSArray<AgoraRtmUserState *> * user_states = response.userStateList;
            } else {
                NSLog(@"getOnlineUsers failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    When there is a large number of users in a channel, you may only care about the total number of online users, and not their identities or temporary status. To get the total channel occupancy, set `includeState` and `includeUserId` in `AgoraRtmPresenceOptions` to `false`.

    <CodeBlockTabs defaultValue="Swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Objective-C">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Swift">
        ```swift
        let presenceOptions = AgoraRtmPresenceOptions()
        presenceOptions.includeState = false
        presenceOptions.includeUserId = false

        // Retrieve the user state of the current channel
        rtm.getPresence()?.getOnlineUsers(channelName: "your_channel", channelType: .message, options: presenceOptions) { response, errorInfo in
            if errorInfo == nil {
                print("getOnlineUsers success!!")

                let userCount = response?.totalOccupancy ?? 0
            } else {
                print("getOnlineUsers failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Objective-C">
        ```objc
        AgoraRtmPresenceOptions* presence_opt = [[AgoraRtmPresence alloc] init];
        presence_opt.includeState = false;
        presence_opt.includeUserId = false;

        [[rtm getPresence] getOnlineUsers:@"your_channel" channelType:AgoraRtmChannelTypeMessage options:presence_opt completion:^(AgoraRtmgetOnlineUsersResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"getOnlineUsers success!!");
                int user_count = response.totalOccupancy;
            } else {
                NSLog(@"getOnlineUsers failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    In this case, only the `totalOccupancy` property in the result is valid while all other fields are empty.

    <CalloutContainer type="info">
      <CalloutDescription>
        You cannot set `includeState` to `true` and `includeUserId` to `false` at the same time. To get the temporary status of users, you must also retrieve their user IDs.
      </CalloutDescription>
    </CalloutContainer>

    ### Get user channels [#get-user-channels-3]

    The `getUserChannels` method enables you to query which channels a user is currently in. This includes the message channels a user has subscribed to and the stream channels they have joined. This method is particularly useful for tracking user paths. Refer to the following sample code:

    <CodeBlockTabs defaultValue="Swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Objective-C">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Swift">
        ```swift
        rtm.getPresence()?.getUserChannels("userId") { response, errorInfo in
            if errorInfo == nil {
                print("getUserChannels success!!")

                let channelCount = response?.totalChannel ?? 0
                let channels = response?.channels ?? []

            } else {
                print("getUserChannels failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Objective-C">
        ```objc
        [[rtm getPresence] getUserChannels:@"userId" completion:^(AgoraRtmGetUserChannelsResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"getUserChannels success!!");
                int channel_count = response.totalChannel;
                NSArray<AgoraRtmChannelInfo *> * channels = response.channels;
            } else {
                NSLog(@"getUserChannels failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    The `getUserChannels` method returns complete query results about the channels and their types that the queried user is in, without pagination.

    ## User status management [#user-status-management-3]

    Signaling enables you to set and delete user status messages for the local user in each channel. The SDK notifies other online users in the channel of these changes through event notifications. This feature is useful in use-cases where user status sharing is required, such as real-time synchronization of the user's microphone status, mood, personal signature, score, and message input status.

    Signaling does not permanently save the status data. When a user unsubscribes from a channel, times out, or exits a channel, the data is deleted. To save user data permanently, refer to [Store user metadata](storage/store-user-metadata).

    When a user's temporary status changes, Signaling triggers the remote state changed event notification in real-time. Users who enable presence in `features` when subscribing to a channel, receive the event notification.

    ### Set status [#set-status-3]

    Using presence, you can set the temporary user status for the local user. When you set the status before subscribing to or joining a channel, the data is cached on the client and does not take effect immediately. The status is updated and corresponding event notifications are triggered when you subscribe to or join a channel. The `setState` method applies to both message and stream channels; use the `channelType` parameter to specify the channel type.

    <CodeBlockTabs defaultValue="Swift">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Swift">
          Swift
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Objective-C">
          Objective-C
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Swift">
        ```swift
        let states: [String: String] = ["key1": "value1", "key2": "value2"]

        rtm.getPresence()?.setState(channelName: "your_channel", channelType: .message, items: states) { response, errorInfo in
            if errorInfo == nil {
                print("setState success!!")
            } else {
                print("setState failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Objective-C">
        ```objc
        NSDictionary *states = @{@"key1": @"value1", @"key2": @"value2"};

        [[rtm getPresence] setState:@"your_channel" channelType:AgoraRtmChannelTypeMessage items:states completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"setState success!!");
            } else {
                NSLog(@"setState failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    When using `setState` to set temporary user status, if the specified key already exists, its value is overwritten by the new value. If the specified key does not exist, a new key-value pair is added.

    ### Get status [#get-status-3]

    To obtain the temporary user status set by a user in a specified channel, use the `getState` method:

    <Tabs defaultValue="swift" groupId="language">
      <TabsList>
        <TabsTrigger value="swift">
          Swift
        </TabsTrigger>

        <TabsTrigger value="objc">
          Objective-C
        </TabsTrigger>
      </TabsList>

      <TabsContent value="swift">
        ```swift
        rtm.getPresence()?.getState(channelName: "Chat_room", channelType: .message, userId: "userid") { response, errorInfo in
            if errorInfo == nil {
                print("getState success!!")

                if let state = response?.state {
                    // process state
                }
            } else {
                print("getState failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```

        If the queried user is not present in the specified channel, a `presenceUserNotExist` error message is returned by the SDK.
      </TabsContent>

      <TabsContent value="objc">
        ```objc
        [[rtm getPresence] getState:@"Chat_room" channelType:AgoraRtmChannelTypeMessage userId:@"userid" completion:^(AgoraRtmPresenceGetStateResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"getState success!!");
                AgoraRtmUserState* state = response.state;
            } else {
                NSLog(@"getState failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```

        Use the `getState` method to obtain the temporary status of other online users in the channel. If the queried user is not present in the specified channel, a `AgoraRtmErrorPresenceUserNotExist` error message is returned by the SDK.
      </TabsContent>
    </Tabs>

    ### Delete status [#delete-status-3]

    Each user can set up to 32 key-value pairs in a channel. To remove items that are no longer needed, call `removeState` with a list of keys. The `removeState` method only deletes temporary user status data for the local user.

    <Tabs defaultValue="swift" groupId="language">
      <TabsList>
        <TabsTrigger value="swift">
          Swift
        </TabsTrigger>

        <TabsTrigger value="objc">
          Objective-C
        </TabsTrigger>
      </TabsList>

      <TabsContent value="swift">
        ```swift
        let keys: [String] = ["key1", "key2"]

        rtm.getPresence()?.removeState("your_channel", channelType: .message, keys: keys) { response, errorInfo in
            if errorInfo == nil {
                print("removeState success!!")
            } else {
                print("removeState failed, errorCode \\(errorInfo!.errorCode), reason \\(errorInfo!.reason)")
            }
        }
        ```

        Both the `setState` and `removeState` methods trigger `remoteStateChanged` event notifications. Users who join the channel with presence enabled in `features` receive event notifications containing full data of the user's temporary status.
      </TabsContent>

      <TabsContent value="objc">
        ```objc
        NSArray<NSString*>* keys = @[@"key1", @"key2"];
        [[rtm getPresence] removeState:@"your_channel" channelType:AgoraRtmChannelTypeMessage keys:keys completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
            if (errorInfo == nil) {
                NSLog(@"removeState success!!");
            } else {
                NSLog(@"removeState failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
            }
        }];
        ```

        Both the `setState` and `removeState` methods trigger `AgoraRtmPresenceEventTypeRemoteStateChanged` event notifications. Users who subscribe to the channel with presence enabled in `features` receive event notifications containing full data of the user's temporary status.
      </TabsContent>
    </Tabs>

    ## Receive presence event notifications [#receive-presence-event-notifications-3]

    A presence event notification returns the [AgoraRtmPresenceEvent](/en/api-reference/api-ref/signaling#configpresenceeventpropsag_platform) data structure, which includes the [AgoraRtmPresenceEventType](/en/api-reference/api-ref/signaling#enumvpresencetypepropsag_platform) parameter.

    To receive presence event notifications, implement an event listener. See [event listeners](/en/api-reference/api-ref/signaling#event-listeners) for details. In addition, include presence in the `features` parameter when subscribing to or joining a channel.

    ### Event notification modes [#event-notification-modes-3]

    The presence event notification mode determines how subscribed users receive event notifications. There are two notification modes:

    * `Announce`: Real-time notification mode
    * `Interval`: Scheduled notification mode

    Set the **Max number of instant event** value in Agora Console to specify the condition for switching between the two modes. The scheduled notification mode helps prevent event noise that results from a large number of online users in the channel. See [Presence configuration](../../manage-agora-account#presence-configuration) for details.

    #### Real-time notification mode [#real-time-notification-mode-3]

    If the number of instant notifications in the channel is less than the **Max number of instant event** value, presence event notifications operate in real-time notification mode. In this mode, the following notifications are sent to the client in real-time:

    **Swift**

    * `remoteJoinChannel`
    * `remoteLeaveChannel`
    * `remoteConnectionTimeout`
    * `remoteStateChanged`

    <CodeBlockTabs defaultValue="Join">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Join">
          Join
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Leave">
          Leave
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Timeout">
          Timeout
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Snapshot">
          Snapshot
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="State change">
          State change
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Join">
        ```js
        {
            type: .remoteJoinChannel;
            channelType: .message;
            channelName: "test_channel";
            publisher: "publisher_name";
            states: [];
            interval: [];
            snapshot: [];
            timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Leave">
        ```js
        {
            eventType: .remoteLeaveChannel;
            channelType: .message;
            channelName: "test_channel";
            publisher: "publisher_name";
            stateChanged: [];
            interval: [];
            snapshot: [];
            timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Timeout">
        ```js
        {
            eventType: .remoteConnectionTimeout;
            channelType: .message;
            channelName: "test_channel";
            publisher: "publisher_name";
            stateChanged: [];
            interval: [];
            snapshot: [];
            timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Snapshot">
        ```js
        {
            eventType: .snapshot;
            channelType: .message;
            channelName: "test_channel";
            publisher: "";
            stateChanged: [];
            interval: [];
            snapshot: [
                { userId: "user_a", states: {}},
                { userId: "user_b", states: { key_1: "value_1" }},
                { userId: "yourSelf", states: {}},
            ];
            timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="State change">
        ```js
        {
            eventType: .remoteStateChanged;
            channelType: .message;
            channelName: "test_channel";
            publisher: "publisher_name";
            states: {
                "key_1": "value_1",
            };
            interval: [];
            snapshot: [];
            timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    **Objective-C**

    * `AgoraRtmPresenceEventTypeRemoteJoinChannel`
    * `AgoraRtmPresenceEventTypeRemoteLeaveChannel`
    * `AgoraRtmPresenceEventTypeRemoteConnectionTimeout`
    * `AgoraRtmPresenceEventTypeRemoteStateChanged`

    <CodeBlockTabs defaultValue="Join">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Join">
          Join
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Leave">
          Leave
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Timeout">
          Timeout
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Snapshot">
          Snapshot
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="State change">
          State change
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Join">
        ```js
        {
              type: AgoraRtmPresenceEventTypeRemoteJoinChannel;
              channelType: AgoraRtmChannelTypeMessage;
              channelName: "test_channel";
              publisher: "publisher_name";
              states: [];
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Leave">
        ```js
        {
              eventType: AgoraRtmPresenceEventTypeRemoteLeaveChannel;
              channelType: AgoraRtmChannelTypeMessage;
              channelName: "test_channel";
              publisher: "publisher_name";
              stateChanged: [];
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Timeout">
        ```js
        {
              eventType: AgoraRtmPresenceEventTypeRemoteConnectionTimeout;
              channelType: AgoraRtmChannelTypeMessage;
              channelName: "test_channel";
              publisher: "publisher_name";
              stateChanged: [];
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Snapshot">
        ```js
        {
              eventType: AgoraRtmPresenceEventTypeSnapshot;
              channelType: AgoraRtmChannelTypeMessage;
              channelName: "test_channel";
              publisher: "";
              stateChanged: [];
              interval: [];
              snapshot: [
                  { userId: "user_a", states: {}},
                  { userId: "user_b", states: { key_1: "value_1" }},
                  { userId: "yourSelf", states: {}},
              ];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="State change">
        ```js
        {
              eventType: AgoraRtmPresenceEventTypeRemoteStateChanged;
              channelType: AgoraRtmChannelTypeMessage;
              channelName: "test_channel";
              publisher: "publisher_name";
              states: {
                  "key_1": "value_1",
              };
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    #### Scheduled notification mode [#scheduled-notification-mode-3]

    <Tabs defaultValue="swift" groupId="language">
      <TabsList>
        <TabsTrigger value="swift">
          Swift
        </TabsTrigger>

        <TabsTrigger value="objc">
          Objective-C
        </TabsTrigger>
      </TabsList>

      <TabsContent value="swift">
        When the number of online users in a channel exceeds the **Max number of instant event** value, the channel switches to the scheduled notification mode. In this mode, `remoteJoinChannel`, `remoteLeaveChannel`, `remoteConnectionTimeout`, and `remoteStateChanged` events are replaced by `interval` events and sent to all users in the channel at specific time intervals. Users receive the following notification:

        ```js
        {
              "type": "AgoraRtmPresenceEventTypeInterval",
              "channelType": "AgoraRtmChannelTypeMessages",
              "channelName": "Chat_room",
              "publisher": "Tony",
              "interval": {
                  "remote_join": ["Tony", "Lily"],
                  "remote_leave": ["Jason"],
                  "remote_timeout": ["Wendy"],
                  "remote_state_change": [
                      {
                          "userId": "Harvard",
                          "states": [
                              { "Mic": "False" },
                              { "Position": "Washington" }
                          ]
                      },
                      {
                          "userId": "Harvard",
                          "states": [
                              { "Mic": "True" },
                              { "Position": "Washington" }
                          ]
                      }
                  ]
              }
        }
        ```
      </TabsContent>

      <TabsContent value="objc">
        When the number of online users in the channel exceeds the **Max number of instant event** value, the channel switches to the scheduled notification mode. In this mode, `AgoraRtmPresenceEventTypeRemoteJoinChannel`, `AgoraRtmPresenceEventTypeRemoteLeaveChannel`, `AgoraRtmPresenceEventTypeRemoteConnectionTimeout`, and `AgoraRtmPresenceEventTypeRemoteStateChanged` events are replaced by `AgoraRtmPresenceEventTypeInterval` events and sent to all users in the channel at specific time intervals. Users receive the following notification:

        ```js
        {
              "type": "AgoraRtmPresenceEventTypeInterval",
              "channelType": "AgoraRtmChannelTypeMessages",
              "channelName": "Chat_room",
              "publisher": "Tony",
              "interval": {
                  "remote_join": ["Tony", "Lily"],
                  "remote_leave": ["Jason"],
                  "remote_timeout": ["Wendy"],
                  "remote_state_change": [
                      {
                          "userId": "Harvard",
                          "states": [
                              { "Mic": "False" },
                              { "Position": "Washington" }
                          ]
                      },
                      {
                          "userId": "Harvard",
                          "states": [
                              { "Mic": "True" },
                              { "Position": "Washington" }
                          ]
                      }
                  ]
              }
        }
        ```
      </TabsContent>
    </Tabs>

    ## Reference [#reference-3]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### API reference [#api-reference-3]

    <Tabs defaultValue="swift" groupId="language">
      <TabsList>
        <TabsTrigger value="swift">
          Swift
        </TabsTrigger>

        <TabsTrigger value="objc">
          Objective-C
        </TabsTrigger>
      </TabsList>

      <TabsContent value="swift">
        * [`getOnlineUsers`](/en/api-reference/api-ref/signaling?platform=ios\&tab=swift#getonlineusers)
        * [`getUserChannels`](/en/api-reference/api-ref/signaling?platform=ios\&tab=swift#getuserchannels)
        * [`setState`](/en/api-reference/api-ref/signaling?platform=ios\&tab=swift#presencesetstatepropsag_platform)
        * [`getState`](/en/api-reference/api-ref/signaling?platform=ios\&tab=swift#presencegetstatepropsag_platform)
        * [`removeState`](/en/api-reference/api-ref/signaling?platform=ios\&tab=swift#presenceremovestatepropsag_platform)
        * [Event listeners](/en/api-reference/api-ref/signaling?platform=ios\&tab=swift#event-listeners)
      </TabsContent>

      <TabsContent value="objc">
        * [`getOnlineUsers`](/en/api-reference/api-ref/signaling?platform=ios\&tab=objc#getonlineusers)
        * [`getUserChannels`](/en/api-reference/api-ref/signaling?platform=ios\&tab=objc#getuserchannels)
        * [`setState`](/en/api-reference/api-ref/signaling?platform=ios\&tab=objc#presencesetstatepropsag_platform)
        * [`getState`](/en/api-reference/api-ref/signaling?platform=ios\&tab=objc#presencegetstatepropsag_platform)
        * [`removeState`](/en/api-reference/api-ref/signaling?platform=ios\&tab=objc#presenceremovestatepropsag_platform)
        * [Event listeners](/en/api-reference/api-ref/signaling?platform=ios\&tab=objc#event-listeners)
      </TabsContent>
    </Tabs>

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="flutter">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="flutter" />

    In Signaling solutions, it is often important to know a user's current online status. For example, in instant messaging, chat applications, and online collaboration tools, users need to see the availability of their contacts. This information is typically displayed as a status message or icon next to a user's name. Presence features in Signaling SDK enable you to monitor join, leave, and status change notifications for users in a channel. Using Presence, you can:

    * Get a list of users currently in a channel and their temporary status data.
    * Get a list of channels a specified user has joined or is subscribed to.
    * Get, set, or remove user statuses.
    * Receive real-time event notifications when users join or leave specified channels.
    * Receive user status change event notifications in real time.

    ## Understand the tech [#understand-the-tech-4]

    Presence provides real-time information about the availability, and the current status of users, for effective communication and collaboration. It enables you to retrieve a list of users in a channel, or to query the list of channels for a specific user. The following figure illustrates how you integrate presence features into your app.

    **Presence workflow**

    ![Presence workflow](https://assets-docs.agora.io/images/signaling/presence-workflow.svg)

    ## Prerequisites [#prerequisites-4]

    Ensure that you have integrated the Signaling SDK in your project, and implemented the framework functionality from the [SDK quickstart](../../quickstart) page.

    <CalloutContainer type="info">
      <CalloutDescription>
        Presence features require Signaling SDK version 2.2.0 or later.
      </CalloutDescription>
    </CalloutContainer>

    ## Implement presence features [#implement-presence-features-4]

    Using presence, you can implement the following features:

    ### Get channel users [#get-channel-users-4]

    To obtain a list of online users in a channel, call `getOnlineUsers`. Depending on your parameter settings, this method returns a list of online user IDs in a channel and their temporary status data, or just the number of online users in the channel. You do not need to join a channel to call this method. This method is applicable to both message channels and stream channels. Use the `channelType` parameter to specify the channel type.

    After obtaining the initial online users list, update it in real time through `presence` event notifications.

    Refer to the following sample code to query the list of online users in a channel and their current status:

    ```dart
    final channelName = 'chat_room';
    try {
        var (status,response) = await rtmClient.getPresence.getOnlineUsers(
            channelName,
            RtmChannelType.message,
            includeUserId:true,
            includeState:true );
        if (status == true){
            print('${status.operation} failed, errorCode: ${status.errorCode}, due to ${status.reason}');
        } else {
            // If a nextPage exists, the next time getOnlineUsers is called, the value of nextPage should be filled in the page field
            var nextPage = response.nextPage;
            var count = response.count;
            var userStateList = response.userStateList;
            userStateList.forEach((userState) {
                print(`${userState.userId} has states: ${userState.states}`);
            });
        }
    } catch (e) {
        print('something went wrong: $e');
    }
    ```

    The `getOnlineUsers` method retrieves one page of data at a time. Each page contains up to 100 online users. If the channel has more than 100 users, the `nextPage` field in the returned `response` contains a bookmark for the next page. After each query, check if `response.getNextPage` is empty to determine if there is more data. To retrieve the next page, set the `page` parameter in `getOnlineUsers` to the value of `response.getNextPage`. Repeat this process until `response.getNextPage` is null. Refer to the following code:

    ```dart
    final channelName = 'chat_room';
    try {
        var (status,response) = await rtmClient.getPresence.getOnlineUsers(
            channelName,
            RtmChannelType.message,
            includeUserId: true,
            includeState: true
            page: 'your_Next_Page_Bookmark'); // Next page bookmark from the previous call
        if (status == true){
            print('${status.operation} failed, errorCode: ${status.errorCode}, due to ${status.reason}');
        } else {
            // If a nextPage exists, the next time getOnlineUsers is called, the value of nextPage should be filled in the page field
            var nextPage = response.nextPage;
            var count = response.count;
            var userStateList = response.userStateList;
            userStateList.forEach((userState) {
                print(`${userState.userId} has states: ${userState.states}`);
            });
        }
    } catch (e) {
        print('something went wrong: $e');
    }
    ```

    When there is a large number of users in a channel, you may only care about the total number of online users, and not their identities or temporary statuses. To get the total channel occupancy, set `includeState` and `includeUserId` in `getOnlineUsers` to `false`.

    ```dart
    final channelName = 'chat_room';
    try {
        var (status,response) = await rtmClient.getPresence.getOnlineUsers(
            channelName,
            RtmChannelType.message,
            includeUserId: false,
            includeState: false);

        if (status == true){
            print('${status.operation} failed, errorCode: ${status.errorCode}, due to ${status.reason}');
        } else {
            var nextPage = response.nextPage;
            var count = response.count;
            print('There are $count occupants in $channelName channel');
        }
    } catch (e) {
        print('something went wrong: $e');
    }
    ```

    In this case, only the `count` property in the response is valid while all other fields are empty.

    <CalloutContainer type="info">
      <CalloutDescription>
        You cannot set `includeState` to `true` and `includeUserId` to `false` at the same time. To get the temporary status of users, you must also retrieve their user IDs.
      </CalloutDescription>
    </CalloutContainer>

    ### Get user channels [#get-user-channels-4]

    The `getUserChannels` method enables you to query which channels a user is currently in. This includes the message channels a user has subscribed to and the stream channels they have joined. This method is particularly useful for tracking user paths. Refer to the following sample code:

    ```dart
    try {
        var (status,response) = await rtmClient.getPresence.getUserChannels("Tony");
        if (status == true){
            print('${status.operation} failed, errorCode: ${status.errorCode}, due to ${status.reason}');
        } else {
            var channels = response.channels;
            var count = response.count;
            channels.forEach((channelInfo) {
                print('${channelInfo.channelName}:${channelInfo.channelType}')
            });
        }
    } catch (e) {
        print('something went wrong: $e');
    }
    ```

    The `getUserChannels` method returns complete query results about the channels and their types that the queried user is in, without pagination.

    ## User status management [#user-status-management-4]

    Signaling enables you to set and delete user status messages for the local user in each channel. The SDK notifies other online users in the channel of these changes through event notifications. This feature is useful in use-cases where user status sharing is required, such as real-time synchronization of the user's microphone status, mood, personal signature, score, and message input status.

    Signaling does not permanently save the status data. When a user unsubscribes from a channel, times out, or exits a channel, the data is deleted. To save user data permanently, use the [Store user metadata](storage/store-user-metadata) feature.

    When a user's temporary status changes, Signaling triggers a `remoteStateChanged` event notification in real time. Users who set `withPresence = true` when joining the channel, receive the event notification.

    ### Set status [#set-status-4]

    Using presence, you can set the temporary user status for the local user. When you set the status before subscribing to or joining a channel, the data is cached on the client and does not take effect immediately. The status is updated and corresponding event notifications are triggered when you subscribe to or join a channel. The `setState` method applies to both message and stream channels; use the `channelType` parameter to specify the channel type.

    ```dart
    var states = {
        'Mode': 'Happy',
        'Mic': 'False',
    };
    final channelName = 'chat_room';
    try {
        var (status,response) = await rtmClient.getPresence.setState(
            channelName,
            RtmChannelType.message,
            states);
        if (status == true){
            print('${status.operation} failed,errorCode: ${status.errorCode},due to ${status.reason}');
        } else {
            print('set states success!');
        }
    } catch (e) {
        print('something went wrong: $e');
    }
    ```

    When using `setState` to set temporary user status, if the specified key already exists, its value is overwritten by the new value. If the specified key does not exist, a new key/value pair is added.

    ### Get status [#get-status-4]

    To obtain the temporary user status set by a user in a specified channel, use the `getState` method:

    ```dart
    final channelName = 'chat_room';
    try {
        var (status,response) = await rtmClient.getPresence.getState(
            channelName,
            RtmChannelType.message,
            'Tony');
        if (status == true){
            print('${status.operation} failed,errorCode: ${status.errorCode},due to ${status.reason}');
        } else {
            var userState = response.state;
            print('States for ${userState.userId}: ${userState.states}');
        }
    } catch (e) {
        print('something went wrong: $e');
    }
    ```

    If the queried user is not present in the specified channel, a `presenceUserNotExist` error message is returned by the SDK.

    ### Delete status [#delete-status-4]

    Each user can set up to 32 key/value pairs in a channel. To remove items that are no longer needed, call `removeState` with a list of keys. The `removeState` method only deletes temporary user status data for the local user.

    ```dart
    final channelName = 'chat_room';
    final states = ['Mode','Mic'];
    try {
        var (status,response) = await rtmClient.getPresence.removeState(
            channelName,
            RtmChannelType.message,
            states: states);
        if (status == true){
            print('${status.operation} failed,errorCode: ${status.errorCode},due to ${status.reason}');
        } else {
            var userState = response.state;
            print('remove states success!');
        }
    } catch (e) {
        print('something went wrong: $e');
    }
    ```

    Both the `setState` and `removeState` methods trigger `remoteStateChanged` event notifications. Users who join the channel with `withPresence` set to `true` receive event notifications containing full data of the user's temporary status.

    ## Receive presence event notifications [#receive-presence-event-notifications-4]

    A presence event notification returns the [PresenceEvent](/en/api-reference/api-ref/signaling#configpresenceeventpropsag_platform) data structure, which includes the [RtmPresenceEventType](/en/api-reference/api-ref/signaling#enumvpresencetypepropsag_platform) parameter.

    To receive presence event notifications, implement an event listener. See [event listeners](/en/api-reference/api-ref/signaling#event-listeners) for details. In addition, set the `withPresence` parameter to `true` when subscribing to or joining a channel.

    ### Event notification modes [#event-notification-modes-4]

    The presence event notification mode determines how subscribed users receive event notifications. There are two notification modes:

    * `Announce`: Real-time notification mode
    * `Interval`: Scheduled notification mode

    You set the **Max number of instant event** value in Agora Console to specify the condition for switching between the two modes. The scheduled notification mode helps prevent event noise that results from a large number of online users in the channel. See [Presence configuration](../../manage-agora-account#presence-configuration) for details.

    #### Real-time notification mode [#real-time-notification-mode-4]

    If the number of instant notifications in the channel is less than the **Max number of instant event** value, presence event notifications operate in real-time notification mode. In this mode, `remoteJoinChannel`, `remoteLeaveChannel`, `remoteTimeout`, and `remoteStateChanged` notifications are sent to the client in real-time.

    <CodeBlockTabs defaultValue="Join">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Join">
          Join
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Leave">
          Leave
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Timeout">
          Timeout
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Snapshot">
          Snapshot
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="State change">
          State change
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Join">
        ```js
        {
            type: RtmPresenceEventType.remoteJoinChannel,
            channelType: RtmChannelType.message,
            channelName: 'chat_room',
            publisher: 'Tony',
            stateItems: [],
            interval: [],
            snapshot: [],
            timestamp: 1710487149497,
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Leave">
        ```js
        {
            type: RtmPresenceEventType.remoteLeaveChannel,
            channelType: RtmChannelType.message,
            channelName: 'chat_room',
            publisher: 'Tony',
            stateItems: [],
            interval: [],
            snapshot: [],
            timestamp: 1710487149497,
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Timeout">
        ```js
        {
            type: RtmPresenceEventType.remoteTimeout,
            channelType: RtmChannelType.message,
            channelName: 'chat_room',
            publisher: 'Tony',
            stateItems: [],
            interval: [],
            snapshot: [],
            timestamp: 1710487149497,
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Snapshot">
        ```js
        {
            type: RtmPresenceEventType.snapshot,
            channelType: RtmChannelType.message,
            channelName: 'chat_room',
            publisher: 'Tony',
            stateItems: [],
            interval: [],
            snapshot: {[
                { userId: "user_a", states: []},
                { userId: "user_b", states: [{ key: 'mic', value : 'on'}]},
                { userId: "yourSelf", states: [{}]},
            ]},
            timestamp: 1710487149497
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="State change">
        ```js
        {
            type: RtmPresenceEventType.remoteStateChanged,
            channelType: RtmChannelType.message,
            channelName: 'chat_room',
            publisher: 'Tony',
            stateItems: [{ key: 'mic', value : 'on'},{ key: 'mode', value : 'happy'}],
            interval: [],
            snapshot: [],
            timestamp: 1710487149497
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    #### Scheduled notification mode [#scheduled-notification-mode-4]

    When the number of online users in the channel exceeds the **Max number of instant event** value, the channel switches to the scheduled notification mode. In this mode, `remoteJoinChannel`, `remoteLeaveChannel`, `remoteTimeout`, and `remoteStateChanged` events are replaced by `interval` events and sent to all users in the channel at specific time intervals. Users receive the following notification:

    ```js
    {
        type : RtmPresenceEventType.interval,
        channelTye : RtmChannelType.message,,
        channelName : 'chat_room',
        publisher : 'Tony',
        interval : {
            joinUserList : {users:['Tony','Lily']},
            leaveUserList :{users:['Jason']},
            timeoutUserList : {users:['Wang']},
            userStateList : [
                {
                    userId : 'Harvard',
                    states :  [{ key: 'mic', value : 'on'},{ key: 'mode', value : 'happy'}],
                },
                {
                    userId : 'Harvard',
                    states :  [{ key: 'mic', value : 'off'},{ key: 'mode', value : 'sad'}],
                }
            ]
        }
    }
    ```

    ## Reference [#reference-4]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### API reference [#api-reference-4]

    * [`getOnlineUsers`](/en/api-reference/api-ref/signaling#getonlineusers)
    * [`getUserChannels`](/en/api-reference/api-ref/signaling#getuserchannels)
    * [`setState`](/en/api-reference/api-ref/signaling#presencesetstatepropsag_platform)
    * [`getState`](/en/api-reference/api-ref/signaling#presencegetstatepropsag_platform)
    * [`removeState`](/en/api-reference/api-ref/signaling#presenceremovestatepropsag_platform)
    * [Event listeners](/en/api-reference/api-ref/signaling#event-listeners)

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="windows">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="windows" />

    In Signaling solutions, it is often important to know a user's current online status. For example, in instant messaging, chat applications, and online collaboration tools, users need to see the availability of their contacts. This information is typically displayed as a status message or icon next to a user's name. Presence features in Signaling SDK enable you to monitor join, leave, and status change notifications for users in a channel. Using Presence, you can:

    * Get a list of users currently in a channel and their temporary status data.
    * Get a list of channels a specified user has joined or is subscribed to.
    * Get, set, or remove user statuses.
    * Receive real-time event notifications when users join or leave specified channels.
    * Receive user status change event notifications in real time.

    ## Understand the tech [#understand-the-tech-5]

    Presence provides real-time information about the availability, and the current status of users, for effective communication and collaboration. It enables you to retrieve a list of users in a channel, or to query the list of channels for a specific user. The following figure illustrates how you integrate presence features into your app.

    **Presence workflow**

    ![Presence workflow](https://assets-docs.agora.io/images/signaling/presence-workflow.svg)

    ## Prerequisites [#prerequisites-5]

    Ensure that you have integrated the Signaling SDK in your project, and implemented the framework functionality from the [SDK quickstart](../../quickstart) page.

    <CalloutContainer type="info">
      <CalloutDescription>
        Presence features require Signaling SDK version 2.2.0 or later.
      </CalloutDescription>
    </CalloutContainer>

    ## Implement presence features [#implement-presence-features-5]

    Using presence, you can implement the following features:

    ### Get channel users [#get-channel-users-5]

    To obtain a list of online users in a channel, call `getOnlineUsers`. Depending on your parameter settings, this method returns a list of online user IDs in a channel and their temporary status data, or just the number of online users in the channel. You do not need to join a channel to call this method. This method is applicable to both message channels and stream channels. Use the `channelType` parameter to specify the channel type.

    After obtaining the initial online users list, update it in real time through `onPresenceEvent` event notifications.

    Refer to the following sample code to query the list of online users in a channel and their current status:

    ```cpp
    PresenceOptions options;
    options.includeState = true;
    options.includeUserId =true;

    uint64_t requestId;
    rtmClient->getPresence()->getOnlineUsers("channelName", RTM_CHANNEL_TYPE_MESSAGE, options, requestId);
    ```

    After you call this method, the SDK triggers the `onGetOnlineUsersResult` callback to return the call result.

    ```cpp
    class RtmEventHandler : public IRtmEventHandler {
        void onGetOnlineUsersResult(const uint64_t requestId, const UserState *userStateList, const size_t count, const char *nextPage, RTM_ERROR_CODE errorCode) override {
            if (errorCode != RTM_ERROR_OK) {
                printf("getOnlineUsers failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode));
            } else {
                printf("getOnlineUsers success\n");
                for (int i = 0 ;i < count; i++) {
                    printf("user: %s\n", userStateList[i].userId);
                    for (int j = 0 ; j < userStateList[i].statesCount; j++) {
                        printf("key: %s value: %s\n", userStateList[i].states[j].key, userStateList[i].states[j].value);
                    }
                }
            }
        }
    };
    ```

    The `getOnlineUsers` method retrieves one page of data at a time. Each page contains up to 100 online users. If the channel has more than 100 users, the `nextPage` field in the returned result contains a bookmark for the next page. After each query, check if `nextPage` is not empty to determine if there is more data. To retrieve the next page, set the `page` field in `PresenceOptions` to the value of `nextPage`. Repeat this process until `nextPage` is null. Refer to the following code:

    ```cpp
    PresenceOptions options;
    options.includeState = true;
    options.includeUserId =true;
    options.page = "your_Next_Page_Bookmark";

    uint64_t requestId;
    rtmClient->getPresence()->getOnlineUsers("channelName", RTM_CHANNEL_TYPE_MESSAGE, options, requestId);
    ```

    When there is a large number of users in a channel, you may only care about the total number of online users, and not their identities or temporary status. To get the total channel occupancy, set `includeState` and `includeUserId` in `PresenceOptions` to `false`.

    ```cpp
    PresenceOptions options;
    options.includeState = false;
    options.includeUserId =false;

    uint64_t requestId;
    rtmClient->getPresence()->getOnlineUsers("channelName", RTM_CHANNEL_TYPE_MESSAGE, options, requestId);
    ```

    In this case, only the `totalOccupancy` property in the result is valid while all other fields are empty.

    <CalloutContainer type="info">
      <CalloutDescription>
        You cannot set `includeState` to `true` and `includeUserId` to `false` at the same time. To get the temporary status of users, you must also retrieve their user IDs.
      </CalloutDescription>
    </CalloutContainer>

    ### Get user channels [#get-user-channels-5]

    The `getUserChannels` method enables you to query which channels a user is currently in. This includes the message channels a user has subscribed to and the stream channels they have joined. This method is particularly useful for tracking user paths. Refer to the following sample code:

    ```cpp
    uint64_t requestId;
    rtmClient->getPresence()->getUserChannels("tony", requestId);
    ```

    After you call this method, the SDK triggers the `onGetUserChannelsResult` callback to return the call result.

    ```cpp
    class RtmEventHandler : public IRtmEventHandler {
        void onGetUserChannelsResult(const uint64_t requestId, const ChannelInfo *channels, const size_t count, RTM_ERROR_CODE errorCode) override {
            if (errorCode != RTM_ERROR_OK) {
                printf("getUserChannels failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode));
            } else {
                printf("getUserChannels success\n");
                for (int i = 0; i < count; i++) {
                    printf("channel: %s channel type: %d\n", channels[i].channelName, channels[i].channelType);
                }
            }
        }
    };
    ```

    The `getUserChannels` method returns complete query results about the channels and their types that the queried user is in, without pagination.

    ## User status management [#user-status-management-5]

    Signaling enables you to set and delete user status messages for the local user in each channel. The SDK notifies other online users in the channel of these changes through event notifications. This feature is useful in use-cases where user status sharing is required, such as real-time synchronization of the user's microphone status, mood, personal signature, score, and message input status.

    Signaling does not permanently save the status data. When a user unsubscribes from a channel, times out, or exits a channel, the data is deleted. To save user data permanently, use the [Store user metadata](storage/store-user-metadata) feature.

    When a user's temporary status changes, Signaling triggers an `RTM_PRESENCE_EVENT_TYPE_REMOTE_STATE_CHANGED` event notification in real-time. Users who set `withPresence = true` when joining the channel, receive the event notification.

    ### Set status [#set-status-5]

    Using presence, you can set the temporary user status for the local user. When you set the status before subscribing to or joining a channel, the data is cached on the client and does not take effect immediately. The status is updated and corresponding event notifications are triggered when you subscribe to or join a channel. The `setState` method applies to both message and stream channels; use the `channelType` parameter to specify the channel type.

    ```cpp
    std::vector<StateItem> items;
    StateItem item;
    item.key = "Mode";
    item.value = "Happy";
    items.emplace_back(item);

    uint64_t requestId;
    rtm_client->getPresence()->setState("channelName", RTM_CHANNEL_TYPE_MESSAGE, items.data(), items.size(), requestId);
    ```

    After you call this method, the SDK triggers the `onPresenceSetStateResult` callback to return the API call result.

    ```cpp
    class RtmEventHandler : public IRtmEventHandler {
        void onPresenceSetStateResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) override {
            if (errorCode != RTM_ERROR_OK) {
                printf("SetState failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode));
            } else {
                printf("SetState success\n");
            }
        }
    };
    ```

    When using `setState` to set temporary user status, if the specified key already exists, its value is overwritten by the new value. If the specified key does not exist, a new key/value pair is added.

    ### Get status [#get-status-5]

    To obtain the temporary user status set by a user in a specified channel, use the `getState` method:

    ```cpp
    uint64_t requestId;
    rtm_client->getPresence()->getState("channelName", RTM_CHANNEL_TYPE_MESSAGE, "tony", requestId);
    ```

    After you call this method, the SDK triggers the `onPresenceGetStateResult` callback to return the API call result.

    ```cpp
    class RtmEventHandler : public IRtmEventHandler {
        void onPresenceGetStateResult(const uint64_t requestId, const UserState &state, RTM_ERROR_CODE errorCode) override {
            if (errorCode != RTM_ERROR_OK) {
                printf("GetState failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode));
            } else {
                printf("GetState user id: %s success\n", state.userId);
                for (int i = 0; i < state.statesCount; i++) {
                    printf("key: %s, value: %s\n", state.states[i].key, state.states[i].value);
                }
            }
        }
    };
    ```

    Use the `getState` method to obtain the temporary status of other online users in the channel. If the queried user is not present in the specified channel, an error message is returned by the SDK.

    ### Delete status [#delete-status-5]

    Each user can set up to 32 key/value pairs in a channel. To remove items that are no longer needed, call `removeState` with a list of keys. The `removeState` method only deletes temporary user status data for the local user.

    ```cpp
    std::vector<const char*> keys;
    keys.push_back("Mode");
    keys.push_back("Mic");

    uint64_t requestId;
    rtmClient->getPresence()->removeState("channelName", RTM_CHANNEL_TYPE_MESSAGE, keys.data(), keys.size(), requestId);
    ```

    After you call this method, the SDK triggers the `onPresenceRemoveStateResult` callback to return the API call result.

    ```cpp
    class RtmEventHandler : public IRtmEventHandler {
    void onPresenceRemoveStateResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) override {
        if (errorCode != RTM_ERROR_OK) {
            printf("RemoveState failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode));
        } else {
            printf("RemoveState success\n");
        }
    }
    };
    ```

    Both the `setState` and `removeState` methods trigger `RTM_PRESENCE_EVENT_TYPE_REMOTE_STATE_CHANGED` event notifications. Users who join the channel with `withPresence` set to `true` receive event notifications containing full data of the user's temporary status.

    ## Receive presence event notifications [#receive-presence-event-notifications-5]

    A presence event notification returns the [PresenceEvent](/en/api-reference/api-ref/signaling#configpresenceeventpropsag_platform) data structure, which includes the [RTM\_PRESENCE\_EVENT\_TYPE.](/en/api-reference/api-ref/signaling#enumvpresencetypepropsag_platform) parameter.

    To receive presence event notifications, implement an event listener. See [event listeners](/en/api-reference/api-ref/signaling#event-listeners) for details. In addition, set the `withPresence` parameter to `true` when subscribing to or joining a channel.

    ### Event notification modes [#event-notification-modes-5]

    The presence event notification mode determines how subscribed users receive event notifications. There are two notification modes:

    * `Announce`: Real-time notification mode
    * `Interval`: Scheduled notification mode

    You set the **Max number of instant event** value in Agora Console to specify the condition for switching between the two modes. The scheduled notification mode helps prevent event noise that results from a large number of online users in the channel. See [Presence configuration](../../manage-agora-account#presence-configuration) for details.

    #### Real-time notification mode [#real-time-notification-mode-5]

    If the number of instant notifications in the channel is less than the **Max number of instant event** value, presence event notifications operate in real-time notification mode. In this mode, `RTM_PRESENCE_EVENT_TYPE_REMOTE_JOIN_CHANNEL`, `RTM_PRESENCE_EVENT_TYPE_REMOTE_LEAVE_CHANNEL`, `RTM_PRESENCE_EVENT_TYPE_REMOTE_TIMEOUT`, and `RTM_PRESENCE_EVENT_TYPE_REMOTE_STATE_CHANGED` notifications are sent to the client in real-time.

    <CodeBlockTabs defaultValue="Join">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Join">
          Join
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Leave">
          Leave
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Timeout">
          Timeout
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Snapshot">
          Snapshot
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="State change">
          State change
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Join">
        ```js
        {
              type: RTM_PRESENCE_EVENT_TYPE_REMOTE_JOIN_CHANNEL;
              channelType: RTM_CHANNEL_TYPE_MESSAGE;
              channelName: "test_channel";
              publisher: "publisher_name";
              states: [];
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Leave">
        ```js
        {
              eventType: RTM_PRESENCE_EVENT_TYPE_REMOTE_LEAVE_CHANNEL;
              channelType: RTM_CHANNEL_TYPE_MESSAGE;
              channelName: "test_channel";
              publisher: "publisher_name";
              stateChanged: [];
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Timeout">
        ```js
        {
              eventType: RTM_PRESENCE_EVENT_TYPE_REMOTE_TIMEOUT;
              channelType: RTM_CHANNEL_TYPE_MESSAGE;
              channelName: "test_channel";
              publisher: "publisher_name";
              stateChanged: [];
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Snapshot">
        ```js
        {
              eventType: RTM_PRESENCE_EVENT_TYPE_SNAPSHOT;
              channelType: RTM_CHANNEL_TYPE_MESSAGE;
              channelName: "test_channel";
              publisher: "";
              stateChanged: [];
              interval: [];
              snapshot: [
                  { userId: "user_a", states: {}},
                  { userId: "user_b", states: { key_1: "value_1" }},
                  { userId: "yourSelf", states: {}},
              ];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="State change">
        ```js
        {
              eventType: RTM_PRESENCE_EVENT_TYPE_REMOTE_STATE_CHANGED;
              channelType: RTM_CHANNEL_TYPE_MESSAGE;
              channelName: "test_channel";
              publisher: "publisher_name";
              states: {
                  "key_1": "value_1",
              };
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    #### Scheduled notification mode [#scheduled-notification-mode-5]

    When the number of online users in the channel exceeds the **Max number of instant event** value, the channel switches to the scheduled notification mode. In this mode, `RTM_PRESENCE_EVENT_TYPE_REMOTE_JOIN_CHANNEL`, `RTM_PRESENCE_EVENT_TYPE_REMOTE_LEAVE_CHANNEL`, `RTM_PRESENCE_EVENT_TYPE_REMOTE_TIMEOUT`, and `RTM_PRESENCE_EVENT_TYPE_REMOTE_STATE_CHANGED` events are replaced by `RTM_PRESENCE_EVENT_TYPE_INTERVAL` events and sent to all users in the channel at specific time intervals. Users receive the following notification:

    ```js
    {
          "type": "RTM_PRESENCE_EVENT_TYPE_INTERVAL",
          "channelType": "RTM_CHANNEL_TYPE_MESSAGE",
          "channelName": "Chat_room",
          "publisher": "Tony",
          "interval": {
              "remote_join": ["Tony", "Lily"],
              "remote_leave": ["Jason"],
              "remote_timeout": ["Wendy"],
              "remote_state_change": [
                  {
                      "userId": "Harvard",
                      "states": [
                          { "Mic": "False" },
                          { "Position": "Washington" }
                      ]
                  },
                  {
                      "userId": "Harvard",
                      "states": [
                          { "Mic": "False" },
                          { "Position": "Washington" }
                      ]
                  }
              ]
          },
          "snapshot": {
              "userStateList": []
          },
          "timestamp": 1710487149497
    }
    ```

    ## Reference [#reference-5]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### API reference [#api-reference-5]

    * [`getOnlineUsers`](/en/api-reference/api-ref/signaling#getonlineusers)
    * [`getUserChannels`](/en/api-reference/api-ref/signaling#getuserchannels)
    * [`setState`](/en/api-reference/api-ref/signaling#presencesetstatepropsag_platform)
    * [`getState`](/en/api-reference/api-ref/signaling#presencegetstatepropsag_platform)
    * [`removeState`](/en/api-reference/api-ref/signaling#presenceremovestatepropsag_platform)
    * [Event listeners](/en/api-reference/api-ref/signaling#event-listeners)

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="linux-cpp">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="linux-cpp" />

    In Signaling solutions, it is often important to know a user's current online status. For example, in instant messaging, chat applications, and online collaboration tools, users need to see the availability of their contacts. This information is typically displayed as a status message or icon next to a user's name. Presence features in Signaling SDK enable you to monitor join, leave, and status change notifications for users in a channel. Using Presence, you can:

    * Get a list of users currently in a channel and their temporary status data.
    * Get a list of channels a specified user has joined or is subscribed to.
    * Get, set, or remove user statuses.
    * Receive real-time event notifications when users join or leave specified channels.
    * Receive user status change event notifications in real time.

    ## Understand the tech [#understand-the-tech-6]

    Presence provides real-time information about the availability, and the current status of users, for effective communication and collaboration. It enables you to retrieve a list of users in a channel, or to query the list of channels for a specific user. The following figure illustrates how you integrate presence features into your app.

    **Presence workflow**

    ![Presence workflow](https://assets-docs.agora.io/images/signaling/presence-workflow.svg)

    ## Prerequisites [#prerequisites-6]

    Ensure that you have integrated the Signaling SDK in your project, and implemented the framework functionality from the [SDK quickstart](../../quickstart) page.

    <CalloutContainer type="info">
      <CalloutDescription>
        Presence features require Signaling SDK version 2.2.0 or later.
      </CalloutDescription>
    </CalloutContainer>

    ## Implement presence features [#implement-presence-features-6]

    Using presence, you can implement the following features:

    ### Get channel users [#get-channel-users-6]

    To obtain a list of online users in a channel, call `getOnlineUsers`. Depending on your parameter settings, this method returns a list of online user IDs in a channel and their temporary status data, or just the number of online users in the channel. You do not need to join a channel to call this method. This method is applicable to both message channels and stream channels. Use the `channelType` parameter to specify the channel type.

    After obtaining the initial online users list, update it in real time through `onPresenceEvent` event notifications.

    Refer to the following sample code to query the list of online users in a channel and their current status:

    ```cpp
    PresenceOptions options;
    options.includeState = true;
    options.includeUserId =true;

    uint64_t requestId;
    rtmClient->getPresence()->getOnlineUsers("channelName", RTM_CHANNEL_TYPE_MESSAGE, options, requestId);
    ```

    After you call this method, the SDK triggers the `onGetOnlineUsersResult` callback to return the call result.

    ```cpp
    class RtmEventHandler : public IRtmEventHandler {
        void onGetOnlineUsersResult(const uint64_t requestId, const UserState *userStateList, const size_t count, const char *nextPage, RTM_ERROR_CODE errorCode) override {
            if (errorCode != RTM_ERROR_OK) {
                printf("getOnlineUsers failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode));
            } else {
                printf("getOnlineUsers success\n");
                for (int i = 0 ;i < count; i++) {
                    printf("user: %s\n", userStateList[i].userId);
                    for (int j = 0 ; j < userStateList[i].statesCount; j++) {
                        printf("key: %s value: %s\n", userStateList[i].states[j].key, userStateList[i].states[j].value);
                    }
                }
            }
        }
    };
    ```

    The `getOnlineUsers` method retrieves one page of data at a time. Each page contains up to 100 online users. If the channel has more than 100 users, the `nextPage` field in the returned result contains a bookmark for the next page. After each query, check if `nextPage` is not empty to determine if there is more data. To retrieve the next page, set the `page` field in `PresenceOptions` to the value of `nextPage`. Repeat this process until `nextPage` is null. Refer to the following code:

    ```cpp
    PresenceOptions options;
    options.includeState = true;
    options.includeUserId =true;
    options.page = "your_Next_Page_Bookmark";

    uint64_t requestId;
    rtmClient->getPresence()->getOnlineUsers("channelName", RTM_CHANNEL_TYPE_MESSAGE, options, requestId);
    ```

    When there is a large number of users in a channel, you may only care about the total number of online users, and not their identities or temporary status. To get the total channel occupancy, set `includeState` and `includeUserId` in `PresenceOptions` to `false`.

    ```cpp
    PresenceOptions options;
    options.includeState = false;
    options.includeUserId =false;

    uint64_t requestId;
    rtmClient->getPresence()->getOnlineUsers("channelName", RTM_CHANNEL_TYPE_MESSAGE, options, requestId);
    ```

    In this case, only the `totalOccupancy` property in the result is valid while all other fields are empty.

    <CalloutContainer type="info">
      <CalloutDescription>
        You cannot set `includeState` to `true` and `includeUserId` to `false` at the same time. To get the temporary status of users, you must also retrieve their user IDs.
      </CalloutDescription>
    </CalloutContainer>

    ### Get user channels [#get-user-channels-6]

    The `getUserChannels` method enables you to query which channels a user is currently in. This includes the message channels a user has subscribed to and the stream channels they have joined. This method is particularly useful for tracking user paths. Refer to the following sample code:

    ```cpp
    uint64_t requestId;
    rtmClient->getPresence()->getUserChannels("tony", requestId);
    ```

    After you call this method, the SDK triggers the `onGetUserChannelsResult` callback to return the call result.

    ```cpp
    class RtmEventHandler : public IRtmEventHandler {
        void onGetUserChannelsResult(const uint64_t requestId, const ChannelInfo *channels, const size_t count, RTM_ERROR_CODE errorCode) override {
            if (errorCode != RTM_ERROR_OK) {
                printf("getUserChannels failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode));
            } else {
                printf("getUserChannels success\n");
                for (int i = 0; i < count; i++) {
                    printf("channel: %s channel type: %d\n", channels[i].channelName, channels[i].channelType);
                }
            }
        }
    };
    ```

    The `getUserChannels` method returns complete query results about the channels and their types that the queried user is in, without pagination.

    ## User status management [#user-status-management-6]

    Signaling enables you to set and delete user status messages for the local user in each channel. The SDK notifies other online users in the channel of these changes through event notifications. This feature is useful in use-cases where user status sharing is required, such as real-time synchronization of the user's microphone status, mood, personal signature, score, and message input status.

    Signaling does not permanently save the status data. When a user unsubscribes from a channel, times out, or exits a channel, the data is deleted. To save user data permanently, use the [Store user metadata](storage/store-user-metadata) feature.

    When a user's temporary status changes, Signaling triggers an `RTM_PRESENCE_EVENT_TYPE_REMOTE_STATE_CHANGED` event notification in real-time. Users who set `withPresence = true` when joining the channel, receive the event notification.

    ### Set status [#set-status-6]

    Using presence, you can set the temporary user status for the local user. When you set the status before subscribing to or joining a channel, the data is cached on the client and does not take effect immediately. The status is updated and corresponding event notifications are triggered when you subscribe to or join a channel. The `setState` method applies to both message and stream channels; use the `channelType` parameter to specify the channel type.

    ```cpp
    std::vector<StateItem> items;
    StateItem item;
    item.key = "Mode";
    item.value = "Happy";
    items.emplace_back(item);

    uint64_t requestId;
    rtm_client->getPresence()->setState("channelName", RTM_CHANNEL_TYPE_MESSAGE, items.data(), items.size(), requestId);
    ```

    After you call this method, the SDK triggers the `onPresenceSetStateResult` callback to return the API call result.

    ```cpp
    class RtmEventHandler : public IRtmEventHandler {
        void onPresenceSetStateResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) override {
            if (errorCode != RTM_ERROR_OK) {
                printf("SetState failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode));
            } else {
                printf("SetState success\n");
            }
        }
    };
    ```

    When using `setState` to set temporary user status, if the specified key already exists, its value is overwritten by the new value. If the specified key does not exist, a new key/value pair is added.

    ### Get status [#get-status-6]

    To obtain the temporary user status set by a user in a specified channel, use the `getState` method:

    ```cpp
    uint64_t requestId;
    rtm_client->getPresence()->getState("channelName", RTM_CHANNEL_TYPE_MESSAGE, "tony", requestId);
    ```

    After you call this method, the SDK triggers the `onPresenceGetStateResult` callback to return the API call result.

    ```cpp
    class RtmEventHandler : public IRtmEventHandler {
        void onPresenceGetStateResult(const uint64_t requestId, const UserState &state, RTM_ERROR_CODE errorCode) override {
            if (errorCode != RTM_ERROR_OK) {
                printf("GetState failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode));
            } else {
                printf("GetState user id: %s success\n", state.userId);
                for (int i = 0; i < state.statesCount; i++) {
                    printf("key: %s, value: %s\n", state.states[i].key, state.states[i].value);
                }
            }
        }
    };
    ```

    Use the `getState` method to obtain the temporary status of other online users in the channel. If the queried user is not present in the specified channel, an error message is returned by the SDK.

    ### Delete status [#delete-status-6]

    Each user can set up to 32 key/value pairs in a channel. To remove items that are no longer needed, call `removeState` with a list of keys. The `removeState` method only deletes temporary user status data for the local user.

    ```cpp
    std::vector<const char*> keys;
    keys.push_back("Mode");
    keys.push_back("Mic");

    uint64_t requestId;
    rtmClient->getPresence()->removeState("channelName", RTM_CHANNEL_TYPE_MESSAGE, keys.data(), keys.size(), requestId);
    ```

    After you call this method, the SDK triggers the `onPresenceRemoveStateResult` callback to return the API call result.

    ```cpp
    class RtmEventHandler : public IRtmEventHandler {
    void onPresenceRemoveStateResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) override {
        if (errorCode != RTM_ERROR_OK) {
            printf("RemoveState failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode));
        } else {
            printf("RemoveState success\n");
        }
    }
    };
    ```

    Both the `setState` and `removeState` methods trigger `RTM_PRESENCE_EVENT_TYPE_REMOTE_STATE_CHANGED` event notifications. Users who join the channel with `withPresence` set to `true` receive event notifications containing full data of the user's temporary status.

    ## Receive presence event notifications [#receive-presence-event-notifications-6]

    A presence event notification returns the [PresenceEvent](/en/api-reference/api-ref/signaling#configpresenceeventpropsag_platform) data structure, which includes the [RTM\_PRESENCE\_EVENT\_TYPE.](/en/api-reference/api-ref/signaling#enumvpresencetypepropsag_platform) parameter.

    To receive presence event notifications, implement an event listener. See [event listeners](/en/api-reference/api-ref/signaling#event-listeners) for details. In addition, set the `withPresence` parameter to `true` when subscribing to or joining a channel.

    ### Event notification modes [#event-notification-modes-6]

    The presence event notification mode determines how subscribed users receive event notifications. There are two notification modes:

    * `Announce`: Real-time notification mode
    * `Interval`: Scheduled notification mode

    You set the **Max number of instant event** value in Agora Console to specify the condition for switching between the two modes. The scheduled notification mode helps prevent event noise that results from a large number of online users in the channel. See [Presence configuration](../../manage-agora-account#presence-configuration) for details.

    #### Real-time notification mode [#real-time-notification-mode-6]

    If the number of instant notifications in the channel is less than the **Max number of instant event** value, presence event notifications operate in real-time notification mode. In this mode, `RTM_PRESENCE_EVENT_TYPE_REMOTE_JOIN_CHANNEL`, `RTM_PRESENCE_EVENT_TYPE_REMOTE_LEAVE_CHANNEL`, `RTM_PRESENCE_EVENT_TYPE_REMOTE_TIMEOUT`, and `RTM_PRESENCE_EVENT_TYPE_REMOTE_STATE_CHANGED` notifications are sent to the client in real-time.

    <CodeBlockTabs defaultValue="Join">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Join">
          Join
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Leave">
          Leave
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Timeout">
          Timeout
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Snapshot">
          Snapshot
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="State change">
          State change
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Join">
        ```js
        {
              type: RTM_PRESENCE_EVENT_TYPE_REMOTE_JOIN_CHANNEL;
              channelType: RTM_CHANNEL_TYPE_MESSAGE;
              channelName: "test_channel";
              publisher: "publisher_name";
              states: [];
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Leave">
        ```js
        {
              eventType: RTM_PRESENCE_EVENT_TYPE_REMOTE_LEAVE_CHANNEL;
              channelType: RTM_CHANNEL_TYPE_MESSAGE;
              channelName: "test_channel";
              publisher: "publisher_name";
              stateChanged: [];
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Timeout">
        ```js
        {
              eventType: RTM_PRESENCE_EVENT_TYPE_REMOTE_TIMEOUT;
              channelType: RTM_CHANNEL_TYPE_MESSAGE;
              channelName: "test_channel";
              publisher: "publisher_name";
              stateChanged: [];
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Snapshot">
        ```js
        {
              eventType: RTM_PRESENCE_EVENT_TYPE_SNAPSHOT;
              channelType: RTM_CHANNEL_TYPE_MESSAGE;
              channelName: "test_channel";
              publisher: "";
              stateChanged: [];
              interval: [];
              snapshot: [
                  { userId: "user_a", states: {}},
                  { userId: "user_b", states: { key_1: "value_1" }},
                  { userId: "yourSelf", states: {}},
              ];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="State change">
        ```js
        {
              eventType: RTM_PRESENCE_EVENT_TYPE_REMOTE_STATE_CHANGED;
              channelType: RTM_CHANNEL_TYPE_MESSAGE;
              channelName: "test_channel";
              publisher: "publisher_name";
              states: {
                  "key_1": "value_1",
              };
              interval: [];
              snapshot: [];
              timestamp: 1710487149497;
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    #### Scheduled notification mode [#scheduled-notification-mode-6]

    When the number of online users in the channel exceeds the **Max number of instant event** value, the channel switches to the scheduled notification mode. In this mode, `RTM_PRESENCE_EVENT_TYPE_REMOTE_JOIN_CHANNEL`, `RTM_PRESENCE_EVENT_TYPE_REMOTE_LEAVE_CHANNEL`, `RTM_PRESENCE_EVENT_TYPE_REMOTE_TIMEOUT`, and `RTM_PRESENCE_EVENT_TYPE_REMOTE_STATE_CHANGED` events are replaced by `RTM_PRESENCE_EVENT_TYPE_INTERVAL` events and sent to all users in the channel at specific time intervals. Users receive the following notification:

    ```js
    {
          "type": "RTM_PRESENCE_EVENT_TYPE_INTERVAL",
          "channelType": "RTM_CHANNEL_TYPE_MESSAGE",
          "channelName": "Chat_room",
          "publisher": "Tony",
          "interval": {
              "remote_join": ["Tony", "Lily"],
              "remote_leave": ["Jason"],
              "remote_timeout": ["Wendy"],
              "remote_state_change": [
                  {
                      "userId": "Harvard",
                      "states": [
                          { "Mic": "False" },
                          { "Position": "Washington" }
                      ]
                  },
                  {
                      "userId": "Harvard",
                      "states": [
                          { "Mic": "False" },
                          { "Position": "Washington" }
                      ]
                  }
              ]
          },
          "snapshot": {
              "userStateList": []
          },
          "timestamp": 1710487149497
    }
    ```

    ## Reference [#reference-6]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### API reference [#api-reference-6]

    * [`getOnlineUsers`](/en/api-reference/api-ref/signaling#getonlineusers)
    * [`getUserChannels`](/en/api-reference/api-ref/signaling#getuserchannels)
    * [`setState`](/en/api-reference/api-ref/signaling#presencesetstatepropsag_platform)
    * [`getState`](/en/api-reference/api-ref/signaling#presencegetstatepropsag_platform)
    * [`removeState`](/en/api-reference/api-ref/signaling#presenceremovestatepropsag_platform)
    * [Event listeners](/en/api-reference/api-ref/signaling#event-listeners)

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="unity">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="unity" />

    In Signaling solutions, it is often important to know a user's current online status. For example, in instant messaging, chat applications, and online collaboration tools, users need to see the availability of their contacts. This information is typically displayed as a status message or icon next to a user's name. Presence features in Signaling SDK enable you to monitor join, leave, and status change notifications for users in a channel. Using Presence, you can:

    * Get a list of users currently in a channel and their temporary status data.
    * Get a list of channels a specified user has joined or is subscribed to.
    * Get, set, or remove user statuses.
    * Receive real-time event notifications when users join or leave specified channels.
    * Receive user status change event notifications in real time.

    ## Understand the tech [#understand-the-tech-7]

    Presence provides real-time information about the availability, and the current status of users, for effective communication and collaboration. It enables you to retrieve a list of users in a channel, or to query the list of channels for a specific user. The following figure illustrates how you integrate presence features into your app.

    **Presence workflow**

    ![Presence workflow](https://assets-docs.agora.io/images/signaling/presence-workflow.svg)

    ## Prerequisites [#prerequisites-7]

    Ensure that you have integrated the Signaling SDK in your project, and implemented the framework functionality from the [SDK quickstart](../../quickstart) page.

    <CalloutContainer type="info">
      <CalloutDescription>
        Presence features require Signaling SDK version 2.2.0 or later.
      </CalloutDescription>
    </CalloutContainer>

    ## Implement presence features [#implement-presence-features-7]

    Using presence, you can implement the following features:

    ### Get channel users [#get-channel-users-7]

    To obtain a list of online users in a channel, call `GetOnlineUsersAsync`. Depending on your parameter settings, this method returns a list of online user IDs in a channel and their temporary status data, or just the number of online users in the channel. You do not need to join a channel to call this method. This method is applicable to both message channels and stream channels. Use the `channelType` parameter to specify the channel type.

    After obtaining the initial online users list, update it in real time through `OnPresenceEvent` event notifications.

    Refer to the following sample code to query the list of online users in a channel and their current status:

    ```csharp
    var channelType = RTM_CHANNEL_TYPE.MESSAGE;
    var options = new PresenceOptions();
    options.withState = true;
    options.withUserId = true;

    var (status,response) = await rtmClient.GetPresence().GetOnlineUsersAsync("Chat_room", channelType, options);
    if (status.Error)
    {
        Debug.Log(string.Format("{0} is failed!}", status.Operation));
        Debug.Log(string.Format("Error code : {0}", status.ErrorCode));
        Debug.Log(string.Format("Due to: {0}", status.Reason));

    }
    else
    {
        Debug.Log(string.Format("You have got {0} users information ", response.TotalOccupancy));
        foreach (UserState user in response.UserStateList)
        {
            Debug.Log("The User ID is:" + user.userId);
            var userstate = user.states;
            foreach(StateItem stateItem in userstate)
            {
                Debug.Log(string.Format("Key: {0}, Value: {1}", stateItem.key,stateItem.value));
            }

        }

        if (response.NextPage != null)
        {
            Debug.Log("you have the next page information waiting for reading!");
        }
    }
    ```

    The `GetOnlineUsersAsync` method retrieves one page of data at a time. Each page contains up to 100 online users. If the channel has more than 100 users, the `NextPage` field in the returned `response` contains a bookmark for the next page. After each query, check if `response.NextPage` is empty to determine if there is more data. To retrieve the next page, set the `page` field in `PresenceOptions` to the value of `response.NextPage`. Repeat this process until `response.NextPage` is null. Refer to the following code:

    ```csharp
    var options = new PresenceOptions();
    options.withState = true;
    options.withUserId = true;
    options.page = "Next_Page_Bookmark";

    var (status,response) = await rtmClient.GetPresence().GetOnlineUsersAsync("Chat_room", RTM_CHANNEL_TYPE.MESSAGE, options);
    // ...
    ```

    When there is a large number of users in a channel, you may only care about the total number of online users, and not their identities or temporary status. To get the total channel occupancy, set `withState` and `withUserId` in `PresenceOptions` to `false`.

    ```csharp
    var options = new PresenceOptions();
    options.withState = false;
    options.withUserId = false;
    var (status,response) = await rtmClient.GetPresence().GetOnlineUsersAsync("Chat_room", RTM_CHANNEL_TYPE.MESSAGE, options);
    if (status.Error)
    {
        Debug.Log(string.Format("{0} is failed!}", status.Operation)    );
        Debug.Log(string.Format("Error code : {0}", status.ErrorCode));
        Debug.Log(string.Format("Due to: {0}", status.Reason));
    }
    else
    {
        Debug.Log(string.Format("{0} users online ", response.TotalOccupancy));
    }
    ```

    In this case, only the `TotalOccupancy` property in the response is valid while all other fields are empty.

    <CalloutContainer type="info">
      <CalloutDescription>
        You cannot set `withState` to `true` and `withUserId` to `false` at the same time. To get the temporary status of users, you must also retrieve their user IDs.
      </CalloutDescription>
    </CalloutContainer>

    ### Get user channels [#get-user-channels-7]

    The `GetUserChannelsAsync` method enables you to query which channels a user is currently in. This includes the message channels a user has subscribed to and the stream channels they have joined. This method is particularly useful for tracking user paths. Refer to the following sample code:

    ```csharp
    var (status,response) = await rtmClient.GetPresence().GetUserChannelsAsync("Tony");
    if (status.Error)
    {
        Debug.Log(string.Format("{0} is failed!}", status.Operation));
        Debug.Log(string.Format("Error code : {0}", status.ErrorCode));
        Debug.Log(string.Format("Due to: {0}", status.Reason));
    }
    else
    {
        Debug.Log(string.Format("User Tony is now in {0} channels ", response.Channels.Length));
        if (response.Channels.Length > 0)
        {
            foreach (ChannelInfo channel in response.Channels)
            {
                Debug.Log(string.Format("Channel Name: {0}, Channel Type:{1}", channel.channelName, channel.channelType));
            }
        }
    }
    ```

    The `GetUserChannelsAsync` method returns complete query results about the channels and their types that the queried user is in, without pagination.

    ## User status management [#user-status-management-7]

    Signaling enables you to set and delete user status messages for the local user in each channel. The SDK notifies other online users in the channel of these changes through event notifications. This feature is useful in use-cases where user status sharing is required, such as real-time synchronization of the user's microphone status, mood, personal signature, score, and message input status.

    Signaling does not permanently save the status data. When a user unsubscribes from a channel, times out, or exits a channel, the data is deleted. To save user data permanently, use the [Store user metadata](storage/store-user-metadata) feature.

    When a user's temporary status changes, Signaling triggers a `REMOTE_STATE_CHANGED` event notification in real time. Users who set `withPresence = true` when joining the channel, receive the event notification.

    ### Set status [#set-status-7]

    Using presence, you can set the temporary user status for the local user. When you set the status before subscribing to or joining a channel, the data is cached on the client and does not take effect immediately. The status is updated and corresponding event notifications are triggered when you subscribe to or join a channel. The `SetStateAsync` method applies to both message and stream channels; use the `channelType` parameter to specify the channel type.

    ```csharp
    StateItem[] myStates = new StateItem[]
    {
        new StateItem("Mode","Happy"),
        new StateItem("Mic","False"),
        new StateItem("Score","100"),
    };
    var (status,response) = await rtmClient.GetPresence().SetStateAsync("Chat_room", RTM_CHANNEL_TYPE.MESSAGE, myStates);
    if (status.Error)
    {
        Debug.Log(string.Format("{0} is failed!}", status.Operation));
        Debug.Log(string.Format("Error code : {0}", status.ErrorCode));
        Debug.Log(string.Format("Due to: {0}", status.Reason));
    }
    else
    {
        Debug.Log("Set State Success!");
    }
    ```

    When using `SetStateAsync` to set temporary user status, if the specified key already exists, its value is overwritten by the new value. If the specified key does not exist, a new key/value pair is added.

    ### Get status [#get-status-7]

    To obtain the temporary user status set by a user in a specified channel, use the `GetStateAsync` method:

    ```csharp
    var (status,response) = await rtmClient.GetPresence().GetStateAsync("Chat_room", RTM_CHANNEL_TYPE.MESSAGE, "Tony");
    if (status.Error)
    {
        Debug.Log(string.Format("{0} is failed!}", status.Operation));
        Debug.Log(string.Format("Error code : {0}", status.ErrorCode));
        Debug.Log(string.Format("Due to: {0}", status.Reason));
    }
    else
    {
        var userState = response.State;
        Debug.Log(string.Format("User:{0}, have stateCount:{1} states",userState.userId, userState.statesCount));
        foreach(StateItem stateItem in userstate.states)
        {
            Debug.Log(string.Format("State Key: {0}, State Value:{1}", stateItem.key,stateItem.value));
        }
    }
    ```

    Use the `GetStateAsync` method to obtain the temporary status of other online users in the channel. If the queried user is not present in the specified channel, an error message is returned by the SDK.

    ### Delete status [#delete-status-7]

    Each user can set up to 32 key/value pairs in a channel. To remove items that are no longer needed, call `RemoveStateAsync` with a list of keys. The `RemoveStateAsync` method only deletes temporary user status data for the local user.

    ```csharp
    string[] keys = new string[] { "mode","Score" };
    var (status,response) = await rtmClient.GetPresence().RemoveStateAsync("Chat_room", RTM_CHANNEL_TYPE.MESSAGE, keys);
    if (status.Error)
    {
        Debug.Log(string.Format("{0} is failed!}", status.Operation));
        Debug.Log(string.Format("Error code : {0}", status.ErrorCode));
        Debug.Log(string.Format("Due to: {0}", status.Reason));
    }
    else
    {
        Debug.Log("Remove State Success!");
    }
    ```

    Both the `SetStateAsync` and `RemoveStateAsync` methods trigger `REMOTE_STATE_CHANGED` event notifications. Users who join the channel with `withPresence` set to `true` receive event notifications containing full data of the user's temporary status.

    ## Receive presence event notifications [#receive-presence-event-notifications-7]

    A presence event notification returns the [PresenceEvent](/en/api-reference/api-ref/signaling#configpresenceeventpropsag_platform) data structure, which includes the [RTM\_PRESENCE\_EVENT\_TYPE](/en/api-reference/api-ref/signaling#enumvpresencetypepropsag_platform) parameter.

    To receive presence event notifications, implement an event listener. See [event listeners](/en/api-reference/api-ref/signaling#event-listeners) for details. In addition, set the `withPresence` parameter to `true` when subscribing to or joining a channel.

    ### Event notification modes [#event-notification-modes-7]

    The presence event notification mode determines how subscribed users receive event notifications. There are two notification modes:

    * `Announce`: Real-time notification mode
    * `Interval`: Scheduled notification mode

    You set the **Max number of instant event** value in Agora Console to specify the condition for switching between the two modes. The scheduled notification mode helps prevent event noise that results from a large number of online users in the channel. See [Presence configuration](../../manage-agora-account#presence-configuration) for details.

    #### Real-time notification mode [#real-time-notification-mode-7]

    If the number of instant notifications in the channel is less than the **Max number of instant event** value, presence event notifications operate in real-time notification mode. In this mode, `REMOTE_JOIN`, `REMOTE_LEAVE`, `REMOTE_TIMEOUT`, and `REMOTE_STATE_CHANGED` notifications are sent to the client in real-time.

    <CodeBlockTabs defaultValue="Join">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="Join">
          Join
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Leave">
          Leave
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="Timeout">
          Timeout
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="State change">
          State change
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="Join">
        ```js
        {
              "type" : RTM_PRESENCE_EVENT_TYPE.REMOTE_JOIN,
              "channelTye" : RTM_CHANNEL_TYPE.MESSAGE,
              "channelName" : "Chat_room",
              "publisher" : "Tony",
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Leave">
        ```js
        {
              "type" : RTM_PRESENCE_EVENT_TYPE.REMOTE_LEAVE,
              "channelTye" : RTM_CHANNEL_TYPE.MESSAGE,
              "channelName" : "Chat_room",
              "publisher" : "Tony",
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="Timeout">
        ```js
        {
              "type" : RTM_PRESENCE_EVENT_TYPE.REMOTE_TIMEOUT,
              "channelTye" : RTM_CHANNEL_TYPE.MESSAGE,
              "channelName" : "Chat_room",
              "publisher" : "Tony",
        }
        ```
      </CodeBlockTab>

      <CodeBlockTab value="State change">
        ```js
        {
              "type" : RTM_PRESENCE_EVENT_TYPE.REMOTE_STATE_CHANGED,
              "channelTye" : RTM_CHANNEL_TYPE.MESSAGE,
              "channelName" : "Chat_room",
              "publisher" : "Tony",
              "stateItems" : [{"Mic":"False"},{"Position":" Washington"}],
              "stateItemCount" : 2
        }
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    #### Scheduled notification mode [#scheduled-notification-mode-7]

    When the number of online users in the channel exceeds the **Max number of instant event** value, the channel switches to the scheduled notification mode. In this mode, `REMOTE_JOIN`, `REMOTE_LEAVE`, `REMOTE_TIMEOUT`, and `REMOTE_STATE_CHANGED` events are replaced by `INTERVAL` events and sent to all users in the channel at specific time intervals. Users receive the following notification:

    ```js
    {
          "type": "RTM_PRESENCE_EVENT_TYPE.INTERVAL",
          "channelType": "RTM_CHANNEL_TYPE.MESSAGE",
          "channelName": "Chat_room",
          "publisher": "Tony",
          "interval": {
              "joinUserList": ["Tony", "Lily"],
              "leaveUserList": ["Jason"],
              "timeoutUserList": ["Wendy"],
              "userStateList": [
                  {
                      "userId": "Harvard",
                      "states": [
                          { "Mic": "False" },
                          { "Position": "Washington" }
                      ]
                  },
                  {
                      "userId": "Harvard",
                      "states": [
                          { "Mic": "False" },
                          { "Position": "Washington" }
                      ]
                  }
              ]
          }
    }
    ```

    ## Reference [#reference-7]

    This section contains content that completes the information on this page, or points you to documentation that explains other aspects to this product.

    ### API reference [#api-reference-7]

    * [`getOnlineUsers`](/en/api-reference/api-ref/signaling#getonlineusers)
    * [`getUserChannels`](/en/api-reference/api-ref/signaling#getuserchannels)
    * [`setState`](/en/api-reference/api-ref/signaling#presencesetstatepropsag_platform)
    * [`getState`](/en/api-reference/api-ref/signaling#presencegetstatepropsag_platform)
    * [`removeState`](/en/api-reference/api-ref/signaling#presenceremovestatepropsag_platform)
    * [Event listeners](/en/api-reference/api-ref/signaling#event-listeners)

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>
</_PlatformTabsGroup>
