diff --git a/.pubnub.yml b/.pubnub.yml
index 72a596f204..c6656e98b6 100644
--- a/.pubnub.yml
+++ b/.pubnub.yml
@@ -1,9 +1,9 @@
name: kotlin
-version: 10.6.0
+version: 11.0.0
schema: 1
scm: github.com/pubnub/kotlin
files:
- - build/libs/pubnub-kotlin-10.6.0-all.jar
+ - build/libs/pubnub-kotlin-11.0.0-all.jar
sdks:
-
type: library
@@ -23,8 +23,8 @@ sdks:
-
distribution-type: library
distribution-repository: maven
- package-name: pubnub-kotlin-10.6.0
- location: https://repo.maven.apache.org/maven2/com/pubnub/pubnub-kotlin/10.6.0/pubnub-kotlin-10.6.0.jar
+ package-name: pubnub-kotlin-11.0.0
+ location: https://repo.maven.apache.org/maven2/com/pubnub/pubnub-kotlin/11.0.0/pubnub-kotlin-11.0.0.jar
supported-platforms:
supported-operating-systems:
Android:
@@ -121,6 +121,19 @@ sdks:
license-url: https://www.apache.org/licenses/LICENSE-2.0.txt
is-required: Required
changelog:
+ - date: 2025-10-21
+ version: v11.0.0
+ changes:
+ - type: feature
+ text: "Added limit and offset parameters for hereNow. Number of returned users per channel by default is limited to 1000. Breaking change."
+ - type: bug
+ text: "Single-channel hereNow with includeUUIDs=false now returns channel data in the channels map for consistency with multi-channel behaviour. Previously, the channels map was empty in this scenario, forcing reliance on totalOccupancy field only. Breaking change."
+ - type: bug
+ text: "Removed possibility to use deprecated MPNS -Microsoft Push Notification Service. Breaking change."
+ - type: bug
+ text: "Added deprecation warning for GCP and old APNS PushType. ."
+ - type: bug
+ text: "In case FCM is chosen as PushType type REST query param gets fcm value instead of gcm."
- date: 2025-09-11
version: v10.6.0
changes:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1556e0a38a..e223d7ec07 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+## v11.0.0
+October 21 2025
+
+#### Added
+- Added limit and offset parameters for hereNow. Number of returned users per channel by default is limited to 1000. Breaking change.
+
+#### Fixed
+- Single-channel hereNow with includeUUIDs=false now returns channel data in the channels map for consistency with multi-channel behaviour. Previously, the channels map was empty in this scenario, forcing reliance on totalOccupancy field only. Breaking change.
+- Removed possibility to use deprecated MPNS -Microsoft Push Notification Service. Breaking change.
+- Added deprecation warning for GCP and old APNS PushType. .
+- In case FCM is chosen as PushType type REST query param gets fcm value instead of gcm.
+
## v10.6.0
September 11 2025
diff --git a/README.md b/README.md
index db825345c4..748687fdb3 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ You will need the publish and subscribe keys to authenticate your app. Get your
com.pubnub
pubnub-kotlin
- 10.6.0
+ 11.0.0
```
diff --git a/gradle.properties b/gradle.properties
index 86bedca76b..4ee8043e5e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -18,7 +18,7 @@ RELEASE_SIGNING_ENABLED=true
SONATYPE_HOST=DEFAULT
SONATYPE_AUTOMATIC_RELEASE=false
GROUP=com.pubnub
-VERSION_NAME=10.6.0
+VERSION_NAME=11.0.0
POM_PACKAGING=jar
POM_NAME=PubNub SDK
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 64676ce99c..306bf957db 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -12,8 +12,8 @@ ktlint = "12.1.0"
dokka = "2.0.0"
kotlinx_datetime = "0.6.2"
kotlinx_coroutines = "1.10.2"
-pubnub_js = "9.8.1"
-pubnub_swift = "9.3.2"
+pubnub_js = "10.1.0"
+pubnub_swift = "9.3.5"
[libraries]
retrofit2 = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit2" }
diff --git a/pubnub-gson/pubnub-gson-api/src/main/java/com/pubnub/api/java/endpoints/presence/HereNow.java b/pubnub-gson/pubnub-gson-api/src/main/java/com/pubnub/api/java/endpoints/presence/HereNow.java
index b44f67387e..02305171a8 100644
--- a/pubnub-gson/pubnub-gson-api/src/main/java/com/pubnub/api/java/endpoints/presence/HereNow.java
+++ b/pubnub-gson/pubnub-gson-api/src/main/java/com/pubnub/api/java/endpoints/presence/HereNow.java
@@ -3,12 +3,79 @@
import com.pubnub.api.java.endpoints.Endpoint;
import com.pubnub.api.models.consumer.presence.PNHereNowResult;
+/**
+ * Obtain information about the current state of channels including a list of unique user IDs
+ * currently subscribed and the total occupancy count.
+ *
+ *
+ * @see com.pubnub.api.models.consumer.presence.PNHereNowResult
+ */
public interface HereNow extends Endpoint {
+ /**
+ * Set the channels to query for presence information.
+ *
+ * @param channels List of channel names to query.
+ * @return {@code this} for method chaining
+ * @see #channelGroups(java.util.List)
+ */
HereNow channels(java.util.List channels);
+ /**
+ * Set the channel groups to query for presence information.
+ *
+ * @param channelGroups List of channel group names to query.
+ * @return {@code this} for method chaining
+ * @see #channels(java.util.List)
+ */
HereNow channelGroups(java.util.List channelGroups);
+ /**
+ * Whether the response should include presence state information, if available.
+ *
+ * @param includeState {@code true} to include state information, {@code false} to exclude. Default: {@code false}
+ * @return {@code this} for method chaining
+ * @see #includeUUIDs(boolean)
+ */
HereNow includeState(boolean includeState);
+ /**
+ * Include the list of UUIDs currently present in each channel.
+ *
+ * @param includeUUIDs {@code true} to include UUID list, {@code false} for occupancy count only. Default: {@code true}
+ * @return {@code this} for method chaining
+ * @see #includeState(boolean)
+ * @see #limit(int)
+ */
HereNow includeUUIDs(boolean includeUUIDs);
+
+ /**
+ * Set the maximum number of occupants to return per channel.
+ *
+ * The server enforces a maximum limit of 1000. Values outside this range will be rejected by the server.
+ *
+ * Special behavior:
+ *
+ * - Use {@code limit = 0} to retrieve only occupancy counts without individual occupant UUIDs
+ *
+ *
+ * @param limit Maximum number of occupants to return (0-1000). Default: 1000
+ * @return {@code this} for method chaining
+ * @see #offset(Integer) for pagination support
+ */
+ HereNow limit(int limit);
+
+ /**
+ * Set the zero-based starting index for pagination through occupants.
+ *
+ * Server-side validation applies:
+ *
+ * - Must be >= 0 (negative values will be rejected)
+ *
+ *
+ *
+ * @param offset Zero-based starting position (must be >= 0). Default: null (no offset)
+ * @return {@code this} for method chaining
+ * @see #limit(int) for controlling result size
+ */
+ HereNow offset(Integer offset);
}
diff --git a/pubnub-gson/pubnub-gson-docs/src/main/java/com/pubnub/docs/presence/HereNowApp.java b/pubnub-gson/pubnub-gson-docs/src/main/java/com/pubnub/docs/presence/HereNowApp.java
index 13879ec6e8..0e80fe4cb6 100644
--- a/pubnub-gson/pubnub-gson-docs/src/main/java/com/pubnub/docs/presence/HereNowApp.java
+++ b/pubnub-gson/pubnub-gson-docs/src/main/java/com/pubnub/docs/presence/HereNowApp.java
@@ -4,6 +4,7 @@
import com.pubnub.api.PubNubException;
import com.pubnub.api.UserId;
+import com.pubnub.api.enums.PNLogVerbosity;
import com.pubnub.api.java.PubNub;
import com.pubnub.api.java.v2.PNConfiguration;
import com.pubnub.api.models.consumer.presence.PNHereNowChannelData;
@@ -17,6 +18,7 @@ public static void main(String[] args) throws PubNubException {
// Configure PubNub instance
PNConfiguration.Builder configBuilder = PNConfiguration.builder(new UserId("demoUserId"), "demo");
configBuilder.publishKey("demo");
+ configBuilder.logVerbosity(PNLogVerbosity.BODY);
configBuilder.secure(true);
PubNub pubnub = PubNub.create(configBuilder.build());
@@ -25,6 +27,8 @@ public static void main(String[] args) throws PubNubException {
pubnub.hereNow()
.channels(Arrays.asList("coolChannel", "coolChannel2"))
.includeUUIDs(true)
+ .limit(100)
+ .offset(10)
.async(result -> {
result.onSuccess((PNHereNowResult res) -> {
diff --git a/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PresenceIntegrationTests.java b/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PresenceIntegrationTests.java
index 66767d4623..b99367275b 100644
--- a/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PresenceIntegrationTests.java
+++ b/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PresenceIntegrationTests.java
@@ -1,6 +1,7 @@
package com.pubnub.api.integration;
import com.google.gson.JsonObject;
+import com.pubnub.api.PubNubException;
import com.pubnub.api.enums.PNHeartbeatNotificationOptions;
import com.pubnub.api.enums.PNStatusCategory;
import com.pubnub.api.integration.util.BaseIntegrationTest;
@@ -11,6 +12,7 @@
import com.pubnub.api.models.consumer.PNStatus;
import com.pubnub.api.models.consumer.presence.PNHereNowChannelData;
import com.pubnub.api.models.consumer.presence.PNHereNowOccupantData;
+import com.pubnub.api.models.consumer.presence.PNHereNowResult;
import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult;
import org.awaitility.Awaitility;
import org.awaitility.Durations;
@@ -20,15 +22,20 @@
import org.junit.Test;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class PresenceIntegrationTests extends BaseIntegrationTest {
@@ -382,4 +389,327 @@ public void status(@NotNull PubNub pubnub, @NotNull PNStatus status) {
.until(() -> subscribeSuccess.get() && heartbeatCallsCount.get() > 2);
}
+ @Test
+ public void testHereNowWithLimit() {
+ final AtomicBoolean success = new AtomicBoolean();
+ final int testLimit = 3;
+ final int totalClientsCount = 6;
+ final String expectedChannel = RandomGenerator.get();
+
+ final List clients = new ArrayList() {{
+ add(pubNub);
+ }};
+ for (int i = 1; i < totalClientsCount; i++) {
+ clients.add(getPubNub());
+ }
+
+ for (PubNub client : clients) {
+ subscribeToChannel(client, expectedChannel);
+ }
+
+ pause(TIMEOUT_MEDIUM);
+
+ pubNub.hereNow()
+ .channels(Collections.singletonList(expectedChannel))
+ .includeUUIDs(true)
+ .limit(testLimit)
+ .async((result) -> {
+ assertFalse(result.isFailure());
+ result.onSuccess(pnHereNowResult -> {
+ assertEquals(1, pnHereNowResult.getTotalChannels());
+ assertEquals(1, pnHereNowResult.getChannels().size());
+ assertTrue(pnHereNowResult.getChannels().containsKey(expectedChannel));
+
+ PNHereNowChannelData channelData = pnHereNowResult.getChannels().get(expectedChannel);
+ assertNotNull(channelData);
+ assertEquals(totalClientsCount, channelData.getOccupancy());
+
+ // With limit=3, we should get only 3 occupants even though 6 are present
+ assertEquals(testLimit, channelData.getOccupants().size());
+
+ success.set(true);
+ });
+ });
+
+ listen(success);
+ }
+
+ @Test
+ public void testHereNowWithOffset() {
+ final AtomicBoolean success = new AtomicBoolean();
+ final int offsetValue = 2;
+ final int totalClientsCount = 5;
+ final String expectedChannel = RandomGenerator.get();
+
+ final List clients = new ArrayList() {{
+ add(pubNub);
+ }};
+ for (int i = 1; i < totalClientsCount; i++) {
+ clients.add(getPubNub());
+ }
+
+ for (PubNub client : clients) {
+ subscribeToChannel(client, expectedChannel);
+ }
+
+ pause(TIMEOUT_MEDIUM);
+
+ pubNub.hereNow()
+ .channels(Collections.singletonList(expectedChannel))
+ .includeUUIDs(true)
+ .offset(offsetValue)
+ .async((result) -> {
+ assertFalse(result.isFailure());
+ result.onSuccess(pnHereNowResult -> {
+ assertEquals(1, pnHereNowResult.getTotalChannels());
+ assertEquals(1, pnHereNowResult.getChannels().size());
+ assertTrue(pnHereNowResult.getChannels().containsKey(expectedChannel));
+
+ PNHereNowChannelData channelData = pnHereNowResult.getChannels().get(expectedChannel);
+ assertNotNull(channelData);
+ assertEquals(totalClientsCount, channelData.getOccupancy());
+
+ // With offset=2, we should get remaining occupants (5 total - 2 skipped = 3 remaining)
+ assertEquals(totalClientsCount - offsetValue, channelData.getOccupants().size());
+
+ success.set(true);
+ });
+ });
+
+ listen(success);
+ }
+
+ @Test
+ public void testHereNowPaginationFlow() throws PubNubException {
+ // 8 users in channel01
+ // 3 users in channel02
+ final int pageSize = 3;
+ final int firstOffset = 0;
+ final int secondOffset = 3;
+ final int totalClientsCount = 11;
+ final int channel01TotalCount = 8;
+ final int channel02TotalCount = 3;
+ final String channel01 = RandomGenerator.get();
+ final String channel02 = RandomGenerator.get();
+
+ final List clients = new ArrayList() {{
+ add(pubNub);
+ }};
+ for (int i = 1; i < channel01TotalCount; i++) {
+ clients.add(getPubNub());
+ }
+
+ for (PubNub client : clients) {
+ subscribeToChannel(client, channel01);
+ }
+
+ for (int i = 0; i < channel02TotalCount; i++) {
+ subscribeToChannel(clients.get(i), channel02);
+ }
+
+ pause(TIMEOUT_MEDIUM);
+
+ final Set allOccupantsInChannel01 = new HashSet<>();
+
+ // First page
+ PNHereNowResult firstPage = pubNub.hereNow()
+ .channels(Arrays.asList(channel01, channel02))
+ .includeUUIDs(true)
+ .limit(pageSize)
+ .sync();
+
+ assertNotNull(firstPage);
+ assertEquals(2, firstPage.getTotalChannels());
+
+ PNHereNowChannelData channel01DataPage01 = firstPage.getChannels().get(channel01);
+ assertNotNull(channel01DataPage01);
+ assertEquals(channel01TotalCount, channel01DataPage01.getOccupancy());
+ assertEquals(totalClientsCount, firstPage.getTotalOccupancy()); // total occupancy across all channels
+ assertEquals(pageSize, channel01DataPage01.getOccupants().size());
+
+ PNHereNowChannelData channel02Data = firstPage.getChannels().get(channel02);
+ assertNotNull(channel02Data);
+ assertEquals(channel02TotalCount, channel02Data.getOccupancy());
+ assertEquals(pageSize, channel02Data.getOccupants().size());
+
+ // Collect UUIDs from first page
+ for (PNHereNowOccupantData occupant : channel01DataPage01.getOccupants()) {
+ allOccupantsInChannel01.add(occupant.getUuid());
+ }
+
+ // Second page using pageSize + firstOffset
+ PNHereNowResult secondPage = pubNub.hereNow()
+ .channels(Collections.singletonList(channel01))
+ .includeUUIDs(true)
+ .limit(pageSize)
+ .offset(pageSize + firstOffset)
+ .sync();
+
+ assertNotNull(secondPage);
+ PNHereNowChannelData channel01DataPage02 = secondPage.getChannels().get(channel01);
+ assertNotNull(channel01DataPage02);
+ assertEquals(channel01TotalCount, channel01DataPage02.getOccupancy());
+ assertEquals(channel01TotalCount, secondPage.getTotalOccupancy()); // only channel01 in results
+ assertEquals(pageSize, channel01DataPage02.getOccupants().size());
+
+
+ assertFalse(secondPage.getChannels().containsKey(channel02));
+
+ // Collect UUIDs from second page (should not overlap with first page)
+ for (PNHereNowOccupantData occupant : channel01DataPage02.getOccupants()) {
+ assertFalse("UUID " + occupant.getUuid() + " already found in first page",
+ allOccupantsInChannel01.contains(occupant.getUuid()));
+ allOccupantsInChannel01.add(occupant.getUuid());
+ }
+
+ // Third page using pageSize + secondOffset
+ PNHereNowResult thirdPage = pubNub.hereNow()
+ .channels(Collections.singletonList(channel01))
+ .includeUUIDs(true)
+ .limit(pageSize)
+ .offset(pageSize + secondOffset)
+ .sync();
+
+ assertNotNull(thirdPage);
+ PNHereNowChannelData channel01DataPage03 = thirdPage.getChannels().get(channel01);
+ assertNotNull(channel01DataPage03);
+ assertEquals(channel01TotalCount, channel01DataPage03.getOccupancy());
+
+ // Should have remaining clients (8 - 3 - 3 = 2)
+ int expectedRemainingCount = channel01TotalCount - (pageSize * 2);
+ assertEquals(expectedRemainingCount, channel01DataPage03.getOccupants().size());
+
+ // Collect UUIDs from third page
+ for (PNHereNowOccupantData occupant : channel01DataPage03.getOccupants()) {
+ assertFalse("UUID " + occupant.getUuid() + " already found",
+ allOccupantsInChannel01.contains(occupant.getUuid()));
+ allOccupantsInChannel01.add(occupant.getUuid());
+ }
+
+ // Verify we got all unique clients
+ assertEquals(channel01TotalCount, allOccupantsInChannel01.size());
+ }
+
+ @Test
+ public void testHereNowPaginationWithEmptyChannels() {
+ final AtomicBoolean success = new AtomicBoolean();
+ final String emptyChannel = RandomGenerator.get();
+ final int pageSize = 10;
+
+ // Don't subscribe any clients to the channel, leaving it empty
+
+ pubNub.hereNow()
+ .channels(Collections.singletonList(emptyChannel))
+ .includeUUIDs(true)
+ .limit(pageSize)
+ .async((result) -> {
+ assertFalse(result.isFailure());
+ result.onSuccess(pnHereNowResult -> {
+ // Empty channels are still included in the response
+ assertEquals(1, pnHereNowResult.getTotalChannels());
+ assertEquals(0, pnHereNowResult.getTotalOccupancy());
+ assertEquals(1, pnHereNowResult.getChannels().size());
+
+ PNHereNowChannelData channelData = pnHereNowResult.getChannels().get(emptyChannel);
+ assertNotNull(channelData);
+ assertEquals(0, channelData.getOccupancy());
+ assertTrue(channelData.getOccupants().isEmpty());
+
+ success.set(true);
+ });
+ });
+
+ listen(success);
+ }
+
+ @Test
+ public void testGlobalHereNowWithLimit() throws PubNubException {
+ final int testLimit = 3;
+ final int totalClientsCount = 6;
+ final String channel01 = RandomGenerator.get();
+ final String channel02 = RandomGenerator.get();
+
+ final List clients = new ArrayList() {{
+ add(pubNub);
+ }};
+ for (int i = 1; i < totalClientsCount; i++) {
+ clients.add(getPubNub());
+ }
+
+ // Subscribe first 3 clients to channel01, all 6 to channel02
+ for (int i = 0; i < 3; i++) {
+ subscribeToChannel(clients.get(i), channel01);
+ }
+ for (PubNub client : clients) {
+ subscribeToChannel(client, channel02);
+ }
+
+ pause(TIMEOUT_MEDIUM);
+
+ // Global hereNow (empty channels list)
+ PNHereNowResult result = pubNub.hereNow()
+ .channels(Collections.emptyList())
+ .includeUUIDs(true)
+ .limit(testLimit)
+ .sync();
+
+ assertNotNull(result);
+
+ // Should include at least our test channels
+ assertTrue(result.getTotalChannels() >= 2);
+ assertTrue(result.getChannels().containsKey(channel01));
+ assertTrue(result.getChannels().containsKey(channel02));
+
+ PNHereNowChannelData channel01Data = result.getChannels().get(channel01);
+ PNHereNowChannelData channel02Data = result.getChannels().get(channel02);
+
+ assertNotNull(channel01Data);
+ assertNotNull(channel02Data);
+ assertEquals(3, channel01Data.getOccupancy());
+ assertEquals(6, channel02Data.getOccupancy());
+
+ // With limit=3, each channel should have at most 3 occupants returned
+ assertTrue(channel01Data.getOccupants().size() <= testLimit);
+ assertTrue(channel02Data.getOccupants().size() <= testLimit);
+ }
+
+ @Test
+ public void testGlobalHereNowWithOffset() throws PubNubException {
+ final int offsetValue = 2;
+ final int totalClientsCount = 5;
+ final String expectedChannel = RandomGenerator.get();
+
+ final List clients = new ArrayList() {{
+ add(pubNub);
+ }};
+ for (int i = 1; i < totalClientsCount; i++) {
+ clients.add(getPubNub());
+ }
+
+ for (PubNub client : clients) {
+ subscribeToChannel(client, expectedChannel);
+ }
+
+ pause(TIMEOUT_MEDIUM);
+
+ // Global hereNow with offset
+ PNHereNowResult result = pubNub.hereNow()
+ .channels(Collections.emptyList())
+ .includeUUIDs(true)
+ .offset(offsetValue)
+ .sync();
+
+ assertNotNull(result);
+
+ // Should include at least our test channel
+ assertTrue(result.getTotalChannels() >= 1);
+ assertTrue(result.getChannels().containsKey(expectedChannel));
+
+ PNHereNowChannelData channelData = result.getChannels().get(expectedChannel);
+ assertNotNull(channelData);
+ assertEquals(totalClientsCount, channelData.getOccupancy());
+
+ // With offset=2, we should get remaining occupants
+ assertTrue(channelData.getOccupants().size() <= totalClientsCount - offsetValue);
+ }
}
diff --git a/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PushIntegrationTest.java b/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PushIntegrationTest.java
index ec3b0a9f25..7e2ca4782b 100644
--- a/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PushIntegrationTest.java
+++ b/pubnub-gson/pubnub-gson-impl/src/integrationTest/java/com/pubnub/api/integration/PushIntegrationTest.java
@@ -44,8 +44,7 @@ protected void onBefore() {
public void testEnumNames() {
assertEquals("apns", PNPushType.APNS.toString());
assertEquals("gcm", PNPushType.GCM.toString());
- assertEquals("gcm", PNPushType.FCM.toString());
- assertEquals("mpns", PNPushType.MPNS.toString());
+ assertEquals("fcm", PNPushType.FCM.toString());
assertEquals("apns2", PNPushType.APNS2.toString());
}
diff --git a/pubnub-gson/pubnub-gson-impl/src/main/java/com/pubnub/internal/java/endpoints/presence/HereNowImpl.java b/pubnub-gson/pubnub-gson-impl/src/main/java/com/pubnub/internal/java/endpoints/presence/HereNowImpl.java
index 2d6f54d462..2bdab9bd6c 100644
--- a/pubnub-gson/pubnub-gson-impl/src/main/java/com/pubnub/internal/java/endpoints/presence/HereNowImpl.java
+++ b/pubnub-gson/pubnub-gson-impl/src/main/java/com/pubnub/internal/java/endpoints/presence/HereNowImpl.java
@@ -16,10 +16,13 @@
@Setter
@Accessors(chain = true, fluent = true)
public class HereNowImpl extends PassthroughEndpoint implements HereNow {
+ public static final int MAX_CHANNEL_OCCUPANTS_LIMIT = 1000;
private List channels = new ArrayList<>();
private List channelGroups = new ArrayList<>();
private boolean includeState = false;
private boolean includeUUIDs = true;
+ private int limit = MAX_CHANNEL_OCCUPANTS_LIMIT;
+ private Integer offset = null;
public HereNowImpl(PubNub pubnub) {
super(pubnub);
@@ -32,7 +35,9 @@ protected Endpoint createRemoteAction() {
channels,
channelGroups,
includeState,
- includeUUIDs
+ includeUUIDs,
+ limit,
+ offset
);
}
}
diff --git a/pubnub-kotlin/pubnub-kotlin-api/config/ktlint/baseline.xml b/pubnub-kotlin/pubnub-kotlin-api/config/ktlint/baseline.xml
index 7f53a42813..de677237b5 100644
--- a/pubnub-kotlin/pubnub-kotlin-api/config/ktlint/baseline.xml
+++ b/pubnub-kotlin/pubnub-kotlin-api/config/ktlint/baseline.xml
@@ -23,16 +23,25 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -42,6 +51,9 @@
+
+
+
@@ -66,17 +78,5 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/PubNubImpl.kt b/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/PubNubImpl.kt
index 43a2d06817..86d73e2a06 100644
--- a/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/PubNubImpl.kt
+++ b/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/PubNubImpl.kt
@@ -353,14 +353,18 @@ class PubNubImpl(private val pubNubObjC: KMPPubNub) : PubNub {
channels: List,
channelGroups: List,
includeState: Boolean,
- includeUUIDs: Boolean
+ includeUUIDs: Boolean,
+ limit: Int,
+ offset: Int?
): HereNow {
return HereNowImpl(
pubnub = pubNubObjC,
channels = channels,
channelGroups = channelGroups,
includeState = includeState,
- includeUUIDs = includeUUIDs
+ includeUUIDs = includeUUIDs,
+ limit = limit,
+ offset = offset
)
}
diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.ios.kt b/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.ios.kt
index e1043b2a3d..87ccd1334c 100644
--- a/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.ios.kt
+++ b/pubnub-kotlin/pubnub-kotlin-api/src/appleMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.ios.kt
@@ -27,7 +27,9 @@ class HereNowImpl(
private val channels: List,
private val channelGroups: List,
private val includeState: Boolean,
- private val includeUUIDs: Boolean
+ private val includeUUIDs: Boolean,
+ private val limit: Int = 1000,
+ private val offset: Int? = null,
) : HereNow {
override fun async(callback: Consumer>) {
pubnub.hereNowWithChannels(
@@ -35,6 +37,9 @@ class HereNowImpl(
channelGroups = channelGroups,
includeState = includeState,
includeUUIDs = includeUUIDs,
+ // todo pass limit and offset once available
+ // limit = limit,
+ // offset = offset,
onSuccess = callback.onSuccessHandler {
PNHereNowResult(
totalChannels = it?.totalChannels()?.toInt() ?: 0,
diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/PubNub.kt b/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/PubNub.kt
index ee388d74ce..6086654a6f 100644
--- a/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/PubNub.kt
+++ b/pubnub-kotlin/pubnub-kotlin-api/src/commonMain/kotlin/com/pubnub/api/PubNub.kt
@@ -171,6 +171,8 @@ expect interface PubNub {
channelGroups: List = emptyList(),
includeState: Boolean = false,
includeUUIDs: Boolean = true,
+ limit: Int = 1000,
+ offset: Int? = null,
): HereNow
fun whereNow(uuid: String = configuration.userId.value): WhereNow
diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/Pubnub.d.kt b/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/Pubnub.d.kt
index b31983b387..0d1bec22be 100644
--- a/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/Pubnub.d.kt
+++ b/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/Pubnub.d.kt
@@ -1307,16 +1307,6 @@ open external class PubNub(config: Any /* UUID | UserId */) {
var isSilent: Boolean
}
- interface MPNSNotificationPayload : BaseNotificationPayload {
- var backContent: String?
-
- var backTitle: String?
-
- var count: Number?
-
- var type: String?
- }
-
interface FCMNotificationPayload : BaseNotificationPayload {
var isSilent: Boolean
var icon: String?
@@ -1326,7 +1316,6 @@ open external class PubNub(config: Any /* UUID | UserId */) {
interface `T$37` {
var apns: Any?
- var mpns: Any?
var fcm: Any?
}
@@ -1344,7 +1333,6 @@ open external class PubNub(config: Any /* UUID | UserId */) {
var body: String?
var apns: APNSNotificationPayload
- var mpns: MPNSNotificationPayload
var fcm: FCMNotificationPayload
fun buildPayload(platforms: Array): Any?
diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/com/pubnub/api/PubNubImpl.kt b/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/com/pubnub/api/PubNubImpl.kt
index 6fd8a1444b..e980f510bd 100644
--- a/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/com/pubnub/api/PubNubImpl.kt
+++ b/pubnub-kotlin/pubnub-kotlin-api/src/jsMain/kotlin/com/pubnub/api/PubNubImpl.kt
@@ -354,11 +354,14 @@ class PubNubImpl(val jsPubNub: PubNubJs) : PubNub {
channels: List,
channelGroups: List,
includeState: Boolean,
- includeUUIDs: Boolean
+ includeUUIDs: Boolean,
+ limit: Int,
+ offset: Int?
): HereNow {
return HereNowImpl(
jsPubNub,
createJsObject {
+ // todo handle limit and offset
this.channels = channels.toTypedArray()
this.channelGroups = channelGroups.toTypedArray()
this.includeState = includeState
diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/PubNub.kt b/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/PubNub.kt
index e0fb0a88e9..8fbfcb8001 100644
--- a/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/PubNub.kt
+++ b/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/PubNub.kt
@@ -492,7 +492,7 @@ actual interface PubNub : StatusEmitter, EventEmitter {
/**
* Enable push notifications on provided set of channels.
*
- * @param pushType Accepted values: FCM, APNS, MPNS, APNS2.
+ * @param pushType Accepted values: FCM, APNS2.
* @see [PNPushType]
* @param channels Channels to add push notifications to.
* @param deviceId The device ID (token) to associate with push notifications.
@@ -512,7 +512,7 @@ actual interface PubNub : StatusEmitter, EventEmitter {
/**
* Request a list of all channels on which push notifications have been enabled using specified [ListPushProvisions.deviceId].
*
- * @param pushType Accepted values: FCM, APNS, MPNS, APNS2. @see [PNPushType]
+ * @param pushType Accepted values: FCM, APNS2. @see [PNPushType]
* @param deviceId The device ID (token) to associate with push notifications.
* @param environment Environment within which device should manage list of channels with enabled notifications
* (works only if [pushType] set to [PNPushType.APNS2]).
@@ -529,7 +529,7 @@ actual interface PubNub : StatusEmitter, EventEmitter {
/**
* Disable push notifications on provided set of channels.
*
- * @param pushType Accepted values: FCM, APNS, MPNS, APNS2. @see [PNPushType]
+ * @param pushType Accepted values: FCM, APNS2. @see [PNPushType]
* @param channels Channels to remove push notifications from.
* @param deviceId The device ID (token) associated with push notifications.
* @param environment Environment within which device should manage list of channels with enabled notifications
@@ -548,7 +548,7 @@ actual interface PubNub : StatusEmitter, EventEmitter {
/**
* Disable push notifications from all channels registered with the specified [RemoveAllPushChannelsForDevice.deviceId].
*
- * @param pushType Accepted values: FCM, APNS, MPNS, APNS2. @see [PNPushType]
+ * @param pushType Accepted values: FCM, APNS2. @see [PNPushType]
* @param deviceId The device ID (token) to associate with push notifications.
* @param environment Environment within which device should manage list of channels with enabled notifications
* (works only if [pushType] set to [PNPushType.APNS2]).
@@ -761,19 +761,26 @@ actual interface PubNub : StatusEmitter, EventEmitter {
* currently subscribed to the channel and the total occupancy count of the channel.
*
* @param channels The channels to get the 'here now' details of.
- * Leave empty for a 'global her now'.
+ * Leave empty for a 'global here now'.
* @param channelGroups The channel groups to get the 'here now' details of.
- * Leave empty for a 'global her now'.
+ * Leave empty for a 'global here now'.
* @param includeState Whether the response should include presence state information, if available.
* Defaults to `false`.
* @param includeUUIDs Whether the response should include UUIDs od connected clients.
* Defaults to `true`.
+ * @param limit Maximum number of occupants to return per channel. Valid range: 0-1000.
+ * - Default: 1000
+ * - Use 0 to get occupancy counts without user details
+ * @param offset Zero-based starting index for pagination. Returns occupants starting from this position in the list. Must be >= 0.
+ * - Default: null (no offset)
*/
actual fun hereNow(
channels: List,
channelGroups: List,
includeState: Boolean,
includeUUIDs: Boolean,
+ limit: Int,
+ offset: Int?,
): HereNow
/**
diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt b/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt
index 6594e1616c..d71c7fa14c 100644
--- a/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt
+++ b/pubnub-kotlin/pubnub-kotlin-api/src/jvmMain/kotlin/com/pubnub/api/endpoints/presence/HereNow.kt
@@ -11,4 +11,6 @@ actual interface HereNow : Endpoint {
val channelGroups: List
val includeState: Boolean
val includeUUIDs: Boolean
+ val limit: Int
+ val offset: Int?
}
diff --git a/pubnub-kotlin/pubnub-kotlin-api/src/nonJvm/kotlin/com/pubnub/api/PubNub.nonJvm.kt b/pubnub-kotlin/pubnub-kotlin-api/src/nonJvm/kotlin/com/pubnub/api/PubNub.nonJvm.kt
index 20dc62ab1a..09fb611dda 100644
--- a/pubnub-kotlin/pubnub-kotlin-api/src/nonJvm/kotlin/com/pubnub/api/PubNub.nonJvm.kt
+++ b/pubnub-kotlin/pubnub-kotlin-api/src/nonJvm/kotlin/com/pubnub/api/PubNub.nonJvm.kt
@@ -158,7 +158,9 @@ actual interface PubNub {
channels: List,
channelGroups: List,
includeState: Boolean,
- includeUUIDs: Boolean
+ includeUUIDs: Boolean,
+ limit: Int,
+ offset: Int?
): HereNow
actual fun whereNow(uuid: String): WhereNow
diff --git a/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/PubNubError.kt b/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/PubNubError.kt
index e855bd4fe3..d034dd563f 100644
--- a/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/PubNubError.kt
+++ b/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/PubNubError.kt
@@ -239,6 +239,7 @@ enum class PubNubError(private val code: Int, val message: String) {
181,
"Channel and/or ChannelGroup contains empty string which is not allowed.",
),
+
;
override fun toString(): String {
diff --git a/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/enums/PNPushType.kt b/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/enums/PNPushType.kt
index 8019a8ee7b..d8f5c5d181 100644
--- a/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/enums/PNPushType.kt
+++ b/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/enums/PNPushType.kt
@@ -6,9 +6,14 @@ enum class PNPushType(private val value: String) {
message = "GCM is deprecated. Use FCM instead."
)
GCM("gcm"),
+
+ @Deprecated(
+ replaceWith = ReplaceWith("APNS2"),
+ message = "APNS is deprecated. Use APNS2 instead."
+ )
APNS("apns"),
- MPNS("mpns"),
- FCM("gcm"),
+
+ FCM("fcm"),
APNS2("apns2"),
;
diff --git a/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadHelper.kt b/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadHelper.kt
index ebff6b0f84..54e8427ce3 100644
--- a/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadHelper.kt
+++ b/pubnub-kotlin/pubnub-kotlin-core-api/src/commonMain/kotlin/com/pubnub/api/models/consumer/push/payload/PushPayloadHelper.kt
@@ -12,7 +12,6 @@ class PushPayloadHelper {
)
var fcmPayload: FCMPayload? = null
var fcmPayloadV2: FCMPayloadV2? = null
- var mpnsPayload: MPNSPayload? = null
var apnsPayload: APNSPayload? = null
fun build(): Map {
@@ -38,13 +37,6 @@ class PushPayloadHelper {
}
}
}
- mpnsPayload?.let {
- it.toMap().run {
- if (isNotEmpty()) {
- put("pn_mpns", this)
- }
- }
- }
commonPayload?.let { putAll(it) }
}
}
@@ -131,26 +123,6 @@ class PushPayloadHelper {
}
}
- class MPNSPayload : PushPayloadSerializer {
- var count: Int? = null
- var backTitle: String? = null
- var title: String? = null
- var backContent: String? = null
- var type: String? = null
- var custom: Map? = null
-
- override fun toMap(): Map {
- return mutableMapOf().apply {
- count?.let { put("count", it) }
- backTitle?.let { put("back_title", it) }
- title?.let { put("title", it) }
- backContent?.let { put("back_content", it) }
- type?.let { put("type", it) }
- custom?.let { putAll(it) }
- }
- }
- }
-
@Deprecated(
replaceWith = ReplaceWith("FCMPayloadV2"),
message = "The legacy GCM/FCM payload is deprecated and will" +
diff --git a/pubnub-kotlin/pubnub-kotlin-docs/src/main/kotlin/com/pubnub/docs/presence/HereNowMain.kt b/pubnub-kotlin/pubnub-kotlin-docs/src/main/kotlin/com/pubnub/docs/presence/HereNowMain.kt
index 899ca616e9..512c99a01e 100644
--- a/pubnub-kotlin/pubnub-kotlin-docs/src/main/kotlin/com/pubnub/docs/presence/HereNowMain.kt
+++ b/pubnub-kotlin/pubnub-kotlin-docs/src/main/kotlin/com/pubnub/docs/presence/HereNowMain.kt
@@ -42,7 +42,9 @@ fun singleChannelHereNow(pubnub: PubNub, channel: String) {
println("\n# Basic hereNow for single channel: $channel")
pubnub.hereNow(
- channels = listOf(channel)
+ channels = listOf(channel),
+ limit = 100,
+ offset = 10
).async { result ->
result.onSuccess { response ->
println("SUCCESS: Retrieved presence information")
diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PresenceIntegrationTests.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PresenceIntegrationTests.kt
index 3834dd491b..b3a9bdd384 100644
--- a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PresenceIntegrationTests.kt
+++ b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PresenceIntegrationTests.kt
@@ -1,6 +1,7 @@
package com.pubnub.api.integration
import com.pubnub.api.PubNub
+import com.pubnub.api.PubNubException
import com.pubnub.api.callbacks.SubscribeCallback
import com.pubnub.api.enums.PNHeartbeatNotificationOptions
import com.pubnub.api.enums.PNStatusCategory
@@ -12,6 +13,7 @@ import com.pubnub.test.CommonUtils.randomValue
import com.pubnub.test.asyncRetry
import com.pubnub.test.await
import com.pubnub.test.listen
+import com.pubnub.test.subscribeNonBlocking
import com.pubnub.test.subscribeToBlocking
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@@ -284,4 +286,595 @@ class PresenceIntegrationTests : BaseIntegrationTest() {
Assert.assertNotNull(interceptedUrl)
assertTrue(interceptedUrl!!.queryParameterNames.contains("ee"))
}
+
+ @Test
+ fun testHereNowWithLimit() {
+ val testLimit = 3
+ val totalClientsCount = 6
+ val expectedChannel = randomChannel()
+
+ val clients =
+ mutableListOf(pubnub).apply {
+ addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList())
+ }
+
+ clients.forEach {
+ it.subscribeNonBlocking(expectedChannel)
+ }
+ Thread.sleep(2000)
+
+ pubnub.hereNow(
+ channels = listOf(expectedChannel),
+ includeUUIDs = true,
+ limit = testLimit,
+ ).asyncRetry { result ->
+ assertFalse(result.isFailure)
+ result.onSuccess {
+ assertEquals(1, it.totalChannels)
+ assertEquals(1, it.channels.size)
+ assertTrue(it.channels.containsKey(expectedChannel))
+
+ val channelData = it.channels[expectedChannel]!!
+ assertEquals(totalClientsCount, channelData.occupancy)
+
+ // With limit=3, we should get only 3 occupants even though 6 are present
+ assertEquals(testLimit, channelData.occupants.size)
+ }
+ }
+ }
+
+ @Test
+ fun testHereNowWithStartFrom() {
+ val offsetValue = 2
+ val totalClientsCount = 5
+ val expectedChannel = randomChannel()
+
+ val clients =
+ mutableListOf(pubnub).apply {
+ addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList())
+ }
+
+ clients.forEach {
+ it.subscribeNonBlocking(expectedChannel)
+ }
+ Thread.sleep(2000)
+
+ pubnub.hereNow(
+ channels = listOf(expectedChannel),
+ includeUUIDs = true,
+ offset = offsetValue,
+ ).asyncRetry { result ->
+ assertFalse(result.isFailure)
+ result.onSuccess {
+ assertEquals(1, it.totalChannels)
+ assertEquals(1, it.channels.size)
+ assertTrue(it.channels.containsKey(expectedChannel))
+
+ val channelData = it.channels[expectedChannel]!!
+ assertEquals(totalClientsCount, channelData.occupancy)
+
+ // With offset=2, we should get remaining occupants (5 total - 2 skipped = 3 remaining)
+ assertEquals(totalClientsCount - offsetValue, channelData.occupants.size)
+ }
+ }
+ }
+
+ @Test
+ fun testHereNowWithStartFromIncludeUUIDSisFalse() {
+ val offsetValue = 2
+ val totalClientsCount = 5
+ val expectedChannel = randomChannel()
+
+ val clients =
+ mutableListOf(pubnub).apply {
+ addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList())
+ }
+
+ clients.forEach {
+ it.subscribeNonBlocking(expectedChannel)
+ }
+ Thread.sleep(2000)
+
+ pubnub.hereNow(
+ channels = listOf(expectedChannel),
+ includeUUIDs = false,
+ offset = offsetValue,
+ ).asyncRetry { result ->
+ assertFalse(result.isFailure)
+ result.onSuccess {
+ assertEquals(1, it.totalChannels)
+ assertEquals(1, it.channels.size) // Channel data is always present (consistent with multi-channel)
+ assertEquals(totalClientsCount, it.totalOccupancy)
+
+ // Verify channel data is present with occupancy but no occupants list
+ val channelData = it.channels[expectedChannel]!!
+ assertEquals(totalClientsCount, channelData.occupancy)
+ assertEquals(0, channelData.occupants.size) // occupants list is empty when includeUUIDs = false
+ }
+ }
+ }
+
+ @Test
+ fun testHereNowPaginationFlow() {
+ // 8 users in channel01
+ // 3 users in channel02
+ val pageSize = 3
+ val firstPageOffset = 0
+ val secondPageOffset = 3
+ val totalClientsCount = 11
+ val channel01TotalCount = 8
+ val channel02TotalCount = 3
+ val channel01 = randomChannel()
+ val channel02 = randomChannel()
+
+ val clients =
+ mutableListOf(pubnub).apply {
+ addAll(generateSequence { createPubNub {} }.take(channel01TotalCount - 1).toList())
+ }
+
+ clients.forEach {
+ it.subscribeNonBlocking(channel01)
+ }
+
+ clients.take(3).forEach {
+ it.subscribeNonBlocking(channel02)
+ }
+
+ Thread.sleep(2000)
+
+ val allOccupantsInChannel01 = mutableSetOf()
+
+ // First page
+ val firstPage = pubnub.hereNow(
+ channels = listOf(channel01, channel02),
+ includeUUIDs = true,
+ limit = pageSize,
+ ).sync()!!
+
+ assertEquals(2, firstPage.totalChannels)
+ val channel01DataPage01 = firstPage.channels[channel01]!!
+ assertEquals(channel01TotalCount, channel01DataPage01.occupancy)
+ assertEquals(totalClientsCount, firstPage.totalOccupancy) // this is totalOccupancy in all pages
+ assertEquals(pageSize, channel01DataPage01.occupants.size)
+ val channel02Data = firstPage.channels[channel02]!!
+ assertEquals(channel02TotalCount, channel02Data.occupancy)
+ assertEquals(pageSize, channel02Data.occupants.size)
+
+ // Collect UUIDs from first page
+ channel01DataPage01.occupants.forEach { allOccupantsInChannel01.add(it.uuid) }
+
+ // Second page using pageSize + firstPageOffset
+ val secondPage = pubnub.hereNow(
+ channels = listOf(channel01),
+ includeUUIDs = true,
+ limit = pageSize,
+ offset = pageSize + firstPageOffset,
+ ).sync()!!
+
+ val channel01DataPage02 = secondPage.channels[channel01]!!
+ assertEquals(channel01TotalCount, channel01DataPage02.occupancy)
+ assertEquals(
+ channel01TotalCount,
+ secondPage.totalOccupancy
+ ) // we get result only from channel01 because there is no more result for channel02
+ assertEquals(pageSize, channel01DataPage02.occupants.size)
+
+ assertFalse(secondPage.channels.containsKey(channel02))
+
+ // Collect UUIDs from second page (should not overlap with first page)
+ channel01DataPage02.occupants.forEach {
+ assertFalse("UUID ${it.uuid} already found in first page", allOccupantsInChannel01.contains(it.uuid))
+ allOccupantsInChannel01.add(it.uuid)
+ }
+
+ // Third page using pageSize + secondPageOffset
+ val thirdPage = pubnub.hereNow(
+ channels = listOf(channel01),
+ includeUUIDs = true,
+ limit = pageSize,
+ offset = pageSize + secondPageOffset,
+ ).sync()!!
+
+ val channel01DataPage03 = thirdPage.channels[channel01]!!
+ assertEquals(channel01TotalCount, channel01DataPage03.occupancy)
+
+ // Should have remaining clients (8 - 3 - 3 = 2)
+ val expectedRemainingCount = channel01TotalCount - (pageSize * 2)
+ assertEquals(expectedRemainingCount, channel01DataPage03.occupants.size)
+
+ // Collect UUIDs from third page
+ channel01DataPage03.occupants.forEach {
+ assertFalse("UUID ${it.uuid} already found", allOccupantsInChannel01.contains(it.uuid))
+ allOccupantsInChannel01.add(it.uuid)
+ }
+
+ // Verify we got all unique clients
+ assertEquals(channel01TotalCount, allOccupantsInChannel01.size)
+ }
+
+ @Test
+ fun testHereNowPaginationFlowIncludeUUIDSisFalse() {
+ // 8 users in channel01
+ // 3 users in channel02
+ val pageSize = 3
+ val totalClientsCount = 11
+ val channel01TotalCount = 8
+ val channel02TotalCount = 3
+ val channel01 = randomChannel()
+ val channel02 = randomChannel()
+
+ val clients =
+ mutableListOf(pubnub).apply {
+ addAll(generateSequence { createPubNub {} }.take(channel01TotalCount - 1).toList())
+ }
+
+ clients.forEach {
+ it.subscribeNonBlocking(channel01)
+ }
+
+ clients.take(3).forEach {
+ it.subscribeNonBlocking(channel02)
+ }
+
+ Thread.sleep(2000)
+
+ // First page
+ val firstPage = pubnub.hereNow(
+ channels = listOf(channel01, channel02),
+ includeUUIDs = false,
+ limit = pageSize,
+ ).sync()!!
+
+ assertEquals(2, firstPage.totalChannels)
+ val channel01Data = firstPage.channels[channel01]!!
+ assertEquals(channel01TotalCount, channel01Data.occupancy)
+ assertEquals(0, channel01Data.occupants.size)
+ assertEquals(totalClientsCount, firstPage.totalOccupancy) // this is totalOccupancy in all pages
+ val channel02Data = firstPage.channels[channel02]!!
+ assertEquals(channel02TotalCount, channel02Data.occupancy)
+ assertEquals(0, channel02Data.occupants.size)
+ }
+
+ @Test
+ fun testHereNowWithLimit0() {
+ val limit = 0
+ val totalClientsCount = 5
+ val expectedChannel = randomChannel()
+
+ // Subscribe multiple clients to the channel
+ val clients =
+ mutableListOf(pubnub).apply {
+ addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList())
+ }
+
+ clients.forEach {
+ it.subscribeNonBlocking(expectedChannel)
+ }
+ Thread.sleep(2000)
+
+ // Query with limit=0 to get occupancy without occupant details
+ pubnub.hereNow(
+ channels = listOf(expectedChannel),
+ includeUUIDs = true,
+ limit = limit,
+ ).asyncRetry { result ->
+ assertFalse(result.isFailure)
+ result.onSuccess {
+ assertEquals(1, it.totalChannels)
+ val channelData = it.channels[expectedChannel]!!
+
+ // Occupancy should reflect actual client count
+ assertEquals(totalClientsCount, channelData.occupancy)
+
+ // With limit=0, occupants list should be empty
+ assertEquals(0, channelData.occupants.size)
+ }
+ }
+ }
+
+ @Test
+ fun testHereNowMultipleChannelsWithLimit0() {
+ val limit = 0
+ val totalClientsCount = 5
+ val channel01 = randomChannel()
+ val channel02 = randomChannel()
+
+ // Subscribe multiple clients to both channels
+ val clients =
+ mutableListOf(pubnub).apply {
+ addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList())
+ }
+
+ clients.forEach {
+ it.subscribeNonBlocking(channel01)
+ it.subscribeNonBlocking(channel02)
+ }
+ Thread.sleep(2000)
+
+ // Query with limit=0 to get occupancy without occupant details for multiple channels
+ pubnub.hereNow(
+ channels = listOf(channel01, channel02),
+ includeUUIDs = true,
+ limit = limit,
+ ).asyncRetry { result ->
+ assertFalse(result.isFailure)
+ result.onSuccess {
+ assertEquals(2, it.totalChannels)
+
+ val channel01Data = it.channels[channel01]!!
+ val channel02Data = it.channels[channel02]!!
+
+ // Occupancy should reflect actual client count for both channels
+ assertEquals(totalClientsCount, channel01Data.occupancy)
+ assertEquals(totalClientsCount, channel02Data.occupancy)
+
+ // With limit=0, occupants list should be empty for both channels
+ assertEquals(0, channel01Data.occupants.size)
+ assertEquals(0, channel02Data.occupants.size)
+ }
+ }
+ }
+
+ @Test
+ fun testGlobalHereNowWithLimit0() {
+ val limit = 0
+ val totalClientsCount = 4
+ val expectedChannel = randomChannel()
+
+ // Subscribe multiple clients to the channel
+ val clients =
+ mutableListOf(pubnub).apply {
+ addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList())
+ }
+
+ clients.forEach {
+ it.subscribeNonBlocking(expectedChannel)
+ }
+ Thread.sleep(2000)
+
+ // Global hereNow with limit=0
+ val result = pubnub.hereNow(
+ channels = emptyList(),
+ includeUUIDs = true,
+ limit = limit,
+ ).sync()!!
+
+ // Should include at least our test channel
+ assertTrue(result.totalChannels >= 1)
+ assertTrue(result.channels.containsKey(expectedChannel))
+
+ val channelData = result.channels[expectedChannel]!!
+ assertEquals(totalClientsCount, channelData.occupancy)
+
+ // With limit=0, occupants list should be empty
+ assertEquals(0, channelData.occupants.size)
+ }
+
+ @Test
+ fun testGlobalHereNowWithLimit() {
+ val testLimit = 3
+ val totalClientsCount = 6
+ val channel01 = randomChannel()
+ val channel02 = randomChannel()
+
+ val clients =
+ mutableListOf(pubnub).apply {
+ addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList())
+ }
+
+ // Subscribe first 3 clients to channel01, all 6 to channel02
+ clients.take(3).forEach {
+ it.subscribeNonBlocking(channel01)
+ }
+ clients.forEach {
+ it.subscribeNonBlocking(channel02)
+ }
+ Thread.sleep(2000)
+
+ // Global hereNow (no channels specified)
+ val result = pubnub.hereNow(
+ channels = emptyList(),
+ includeUUIDs = true,
+ limit = testLimit,
+ ).sync()!!
+
+ // Should include at least our test channels
+ assertTrue(result.totalChannels >= 2)
+ assertTrue(result.channels.containsKey(channel01))
+ assertTrue(result.channels.containsKey(channel02))
+
+ val channel01Data = result.channels[channel01]!!
+ val channel02Data = result.channels[channel02]!!
+
+ assertEquals(3, channel01Data.occupancy)
+ assertEquals(6, channel02Data.occupancy)
+
+ // With limit=3, each channel should have at most 3 occupants returned
+ assertTrue(channel01Data.occupants.size <= testLimit)
+ assertTrue(channel02Data.occupants.size <= testLimit)
+ }
+
+ @Test
+ fun testGlobalHereNowWithOffset() {
+ val offsetValue = 2
+ val totalClientsCount = 5
+ val expectedChannel = randomChannel()
+
+ val clients =
+ mutableListOf(pubnub).apply {
+ addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList())
+ }
+
+ clients.forEach {
+ it.subscribeNonBlocking(expectedChannel)
+ }
+ Thread.sleep(2000)
+
+ // Global hereNow with offset
+ val result = pubnub.hereNow(
+ channels = emptyList(),
+ includeUUIDs = true,
+ offset = offsetValue,
+ ).sync()!!
+
+ // Should include at least our test channel
+ assertTrue(result.totalChannels >= 1)
+ assertTrue(result.channels.containsKey(expectedChannel))
+
+ val channelData = result.channels[expectedChannel]!!
+ assertEquals(totalClientsCount, channelData.occupancy)
+
+ // With offset=2, we should get remaining occupants
+ assertTrue(channelData.occupants.size <= totalClientsCount - offsetValue)
+ }
+
+ @Test
+ fun testGlobalHereNowWithNoActiveChannels() {
+ // Don't subscribe any clients, making it a truly empty global query
+ // Wait a bit to ensure no residual presence state from other tests
+
+ val result = pubnub.hereNow(
+ channels = emptyList(),
+ includeUUIDs = true,
+ limit = 10,
+ ).sync()!!
+
+ // Should have no channels
+ // Note: In a shared test environment, there might be residual presence state
+ assertTrue(result.totalOccupancy >= 0)
+ }
+
+ @Test
+ fun testHereNowWithChannelGroupPagination() {
+ val testLimit = 3
+ val totalClientsCount = 6
+ val channelGroupName = randomValue()
+ val channel01 = randomChannel()
+ val channel02 = randomChannel()
+
+ // Create channel group and add channels to it
+ pubnub.addChannelsToChannelGroup(
+ channels = listOf(channel01, channel02),
+ channelGroup = channelGroupName,
+ ).sync()
+
+ Thread.sleep(1000) // Wait for channel group to be created
+
+ // Subscribe clients to the channels
+ val clients = mutableListOf(pubnub).apply {
+ addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList())
+ }
+
+ // Subscribe all clients to channel01, first 3 to channel02
+ clients.forEach {
+ it.subscribeNonBlocking(channel01)
+ }
+ clients.take(3).forEach {
+ it.subscribeNonBlocking(channel02)
+ }
+
+ Thread.sleep(2000) // Wait for presence to register
+
+ // Query hereNow with channel group and limit
+ val result = pubnub.hereNow(
+ channelGroups = listOf(channelGroupName),
+ includeUUIDs = true,
+ limit = testLimit,
+ ).sync()!!
+
+ // Verify results
+ assertEquals(2, result.totalChannels)
+ assertTrue(result.channels.containsKey(channel01))
+ assertTrue(result.channels.containsKey(channel02))
+
+ val channel01Data = result.channels[channel01]!!
+ val channel02Data = result.channels[channel02]!!
+
+ // Verify occupancy counts
+ assertEquals(totalClientsCount, channel01Data.occupancy)
+ assertEquals(3, channel02Data.occupancy)
+
+ // Verify pagination: with limit=3, each channel should return at most 3 occupants
+ assertTrue(channel01Data.occupants.size <= testLimit)
+ assertTrue(channel02Data.occupants.size <= testLimit)
+ assertEquals(testLimit, channel01Data.occupants.size) // channel01 has 6 users, should return 3
+ assertEquals(testLimit, channel02Data.occupants.size) // channel02 has 3 users, should return 3
+
+ // Test with offset
+ val resultWithOffset = pubnub.hereNow(
+ channels = emptyList(),
+ channelGroups = listOf(channelGroupName),
+ includeUUIDs = true,
+ limit = testLimit,
+ offset = 2,
+ ).sync()!!
+
+ val channel01DataWithOffset = resultWithOffset.channels[channel01]!!
+ assertEquals(totalClientsCount, channel01DataWithOffset.occupancy)
+ // With offset=2 and limit=3, we should get 3 occupants (skipping first 2)
+ assertEquals(testLimit, channel01DataWithOffset.occupants.size)
+
+ // Cleanup: remove channel group
+ pubnub.deleteChannelGroup(
+ channelGroup = channelGroupName,
+ ).sync()
+ }
+
+ @Test
+ fun testHereNowWithLimitAbove1000() {
+ val limitAboveMax = 2000
+ val totalClientsCount = 5
+ val expectedChannel = randomChannel()
+
+ val clients =
+ mutableListOf(pubnub).apply {
+ addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList())
+ }
+
+ clients.forEach {
+ it.subscribeNonBlocking(expectedChannel)
+ }
+ Thread.sleep(2000)
+
+ // This should not throw a client-side validation error
+ // Server will validate the limit and respond accordingly
+ pubnub.hereNow(
+ channels = listOf(expectedChannel),
+ includeUUIDs = true,
+ limit = limitAboveMax,
+ ).asyncRetry { result ->
+ assertTrue(result.isFailure)
+ result.onFailure { exception: PubNubException ->
+ assertTrue(exception.message!!.contains("Cannot return more than 1000 uuids at a time"))
+ }
+ }
+ }
+
+ @Test
+ fun testHereNowWithLimitBelow0() {
+ val limitBelow0 = -1
+ val totalClientsCount = 5
+ val expectedChannel = randomChannel()
+
+ val clients =
+ mutableListOf(pubnub).apply {
+ addAll(generateSequence { createPubNub {} }.take(totalClientsCount - 1).toList())
+ }
+
+ clients.forEach {
+ it.subscribeNonBlocking(expectedChannel)
+ }
+ Thread.sleep(2000)
+
+ pubnub.hereNow(
+ channels = listOf(expectedChannel),
+ includeUUIDs = true,
+ limit = limitBelow0,
+ ).asyncRetry { result ->
+ assertTrue(result.isFailure)
+ result.onFailure { exception: PubNubException ->
+ assertTrue(exception.message!!.contains("Limit must be greater than or equal to 0"))
+ }
+ }
+ }
}
diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushIntegrationTest.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushIntegrationTest.kt
index 2f14c6a75b..fdb2b814ca 100644
--- a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushIntegrationTest.kt
+++ b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushIntegrationTest.kt
@@ -26,8 +26,7 @@ class PushIntegrationTest : BaseIntegrationTest() {
@Test
fun testEnumNames() {
assertEquals("apns", PNPushType.APNS.toParamString())
- assertEquals("gcm", PNPushType.FCM.toParamString())
- assertEquals("mpns", PNPushType.MPNS.toParamString())
+ assertEquals("fcm", PNPushType.FCM.toParamString())
assertEquals("apns2", PNPushType.APNS2.toParamString())
}
diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushPayloadHelperIntegrationTest.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushPayloadHelperIntegrationTest.kt
deleted file mode 100644
index 9826b330f0..0000000000
--- a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/api/integration/PushPayloadHelperIntegrationTest.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.pubnub.api.integration
-
-import com.google.gson.Gson
-import com.google.gson.JsonObject
-import com.pubnub.api.PubNub
-import com.pubnub.api.callbacks.SubscribeCallback
-import com.pubnub.api.models.consumer.PNStatus
-import org.junit.Test
-import java.util.UUID
-
-class PushPayloadHelperIntegrationTest : BaseIntegrationTest() {
- @Test
- fun testIntercept() {
- val expectedChannel = UUID.randomUUID().toString()
-
- val payload = Gson().fromJson(json, JsonObject::class.java)
-
- pubnub.addListener(
- object : SubscribeCallback() {
- override fun status(
- pubnub: PubNub,
- pnStatus: PNStatus,
- ) {
- pubnub.publish(
- channel = expectedChannel,
- message = payload,
- ).sync()
- }
- },
- )
-
- pubnub.subscribe(
- channels = listOf(expectedChannel),
- withPresence = true,
- )
-
- wait()
- }
-
- private val json =
- """
- {
- "match": {
- "tournament": "Barclay's Premier League",
- "date": "2018-04-05 13:30:45",
- "venue": "Anfield Road",
- "title": "Goal! 90 min",
- "summary": "Liverpool - Chelsea 2:1"
- },
- "pn_gcm": {
- "data": {
- "title": "Goal! 90 min",
- "summary": "Liverpool - Chelsea 2:1"
- }
- },
- "pn_apns": {
- "aps": {
- "title": "Goal! 90 min",
- "summary": "Liverpool - Chelsea 2:1"
- }
- },
- "pn_mpns": {
- "title": "Goal! 90 min",
- "summary": "Liverpool - Chelsea 2:1"
- }
- }
- """.trimIndent()
-}
diff --git a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/test/Extensions.kt b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/test/Extensions.kt
index c6aab72a48..15d9ffc304 100644
--- a/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/test/Extensions.kt
+++ b/pubnub-kotlin/pubnub-kotlin-impl/src/integrationTest/kotlin/com/pubnub/test/Extensions.kt
@@ -45,6 +45,7 @@ fun AtomicBoolean.listen(function: () -> Boolean): AtomicBoolean {
fun