Dart

Sariska provides a Dart API and easy-to-integrate SDKs for building real-time features into your applications. Easily add in-app chats, instant messaging, and other real-time functionalities to your apps.

Key Features:

  • Real-time messaging for in-app chats and instant messaging

  • Easy installation

  • Socket creation and management

  • Channel creation, joining, and leaving

  • Sending messages, poll votes, and message replies

  • Presence management (track: typing, joining and leaving users)

  • History management (fetching chat history and specific messages)


Installation

Step 1 : Install the Phoenix Client

  • With Dart

dart pub add phoenix_wings

  • With Flutter

flutter pub add phoenix_wings

Step 2 : Create Socket

Establish a WebSocket connection to the Sariska server to join channels, receive events, and send messages.

// Replace {your-token} with your actual authentication token
let token = {your-token};
// Create PhoenixSocketOptions with the authentication token
final options = PhoenixSocketOptions(params: {"token": token});
// Create a PhoenixSocket instance for the Sariska WebSocket endpoint
final socket = PhoenixSocket("wss://api.sariska.io/api/v1/messaging/websocket", socketOptions: options);
// Called when the connection is successfully opened
socket.onOpen( () { print("Connection opened successfully!") } )
// Called when an error occurs during the connection
socket.onError( () { print("There was an error with the connection!") } )
// Called when the connection is closed
socket.onClose( () { print("Connection dropped!") } )

// Initiate the connection to the socket server 
await socket.connect()

To disconnect the user from any previous WebSocket connections and tabs before opening a new one, set the disconnect_past_tabs parameter to true in the WebSocket connection.

Disconnect Socket

Close the WebSocket connection to the Sariska server. This will terminate all active channels and prevent further communication with the server.

// Close connection to the server
await socket.disconnect()

Step 3 : Create Channel

Channels cannot be created directly; instead, they are created through the socket object by calling socket.channel(topic) with the topic of the channel. The topic is a string that identifies the channel, and it can be any string you like.

Channel Prefix

Each channel name starts with a prefix that determines the message serialization format and persistence behavior.

chat: Use this prefix for persisting messages to the database. This prefix requires a fixed schema and is ideal for chat applications.

final channel = socket.channel("chat:room123");

rtc: Use this prefix for scenarios where message persistence is not necessary. This prefix allows sending arbitrary JSON data, making it suitable for events in multiplayer games, IoT applications, and others.

final channel = socket.channel("rtc:room123");

sariska: Use this prefix for performance-critical applications that utilize Flatbuffers as the serialization format and do not require message persistence. This prefix provides zero-copy serialization/deserialization, significantly enhancing performance.

final channel = socket.channel("sariska:room123");

Handle Errors

When an error occurs on the channel, the onError callback is triggered. The callback receives the error information in payload, if available.

channel.onError( () => print("There was an error!") )

Close Channel

When the channel is closed, the onClose callback is invoked. This signifies that the communication on the channel has ended and no further data can be exchanged.

channel.onClose( () => print("Channel closed") )

Step 4 : Join and Leave Channel

To join a channel, call the join() method on the channel object. The join() method returns a promise that resolves when the client has successfully joined the channel. When sending data, you can utilize the .receive() hook to monitor the status of the outgoing push message.

// Join the Channel
channel.join()
    .receive("ok", ()=>print("Channel Joined"))
    .receive("error", ()=>print("Failed to join"))

// Leave the channel
channel.leave()

Channel User Joined

channel.on("user_joined", (payload)=>{
    print(payload["user"]);
    print(payload["room"]);
    });
Payload of user containing user details, including:
  • id: The user's ID

  • name: The user's name

  • avatar: The user's avatar URL

  • email: The user's email address

Payload of room containing room details, including:
  • id: The room's ID

  • session_id: The room's session ID

  • created_by: The ID of the user who created the room

  • status: The status of the room (e.g., "active", "inactive")

Channel New Message

channel.on("new_message", (message)=>{
    print(message)
});
Payload of message containing message details, including:
  • content: The message content

  • id: The sender's user ID

  • name: The sender's name

  • content_type: The message content type (media, emoji, text)

  • is_delivered: Whether the message has been delivered to the recipient

  • created_by: The ID of the user who sent the message

  • created_by_name: The name of the user who sent the message

  • timestamp: The message timestamp

Handle Message

Receive all events for specialized message processing prior to dispatching to channel-specific callbacks. Ensure the payload is returned, regardless of modification status.

channel.onMessage((event, payload, ref) =>{
    var modifiedPayload = payload;
    modifiedPayload["modified"] = true;
    return modifiedPayload;
})

Send Message

Once you've established a connection to a channel, you can start sending messages to other connected clients. To send a message, use the push() method on the channel object.

// Send the new_vote event with the payload
channel
    .push("new_message", payload)
    .receive("ok", (response) {
      print("Poll Vote Sent: $response");
    })
    .receive("error", (response) {
      print("Error: $response");
    });
Payload of new_message containing message details, including:
  • content: The message content

  • type: The message content type (media, emoji, text)

Send Message Reply

var payload = {
  "content": "hi",
  "message_id": 1,
};

channel
    .push("new_message_reply", payload)
    .receive("ok", (response) {
      print("Poll Vote Sent: $response");
    })
    .receive("error", (response) {
      print("Error: $response");
    });

Channel Poll Vote

var pollQuestion = "What is your favorite color?";
var pollOptions = ["Red", "Blue", "Green"];

// Build the payload with the poll question, content type, and options
var payload = {
  "content": pollQuestion,
  "content_type": "poll",
  "options": pollOptions,
};

// Send the new_vote event with the payload
channel
    .push("new_vote", payload)
    .receive("ok", (response) {
      print("Poll Vote Sent: $response");
    })
    .receive("error", (response) {
      print("Error: $response");
    });
Payload of vote containing vote details, including:
  • content: The vote question

  • content_type: The vote content type (media, emoji, text)

  • options: An array of vote options

For other polls APIs, please refer to Swagger documentation

Attach Media Files to Chat Messages

Attaching media files to chat messages involves obtaining a presigned URL, uploading the file to the presigned URL, and then sending the file information to the chat server.

Install Dependencies

import 'package:file_picker/file_picker.dart';
import 'package:http/http.dart' as http;

Obtain a Presigned URL

To obtain a presigned URL, make a POST request to the API endpoint. The request payload should be empty, and the Authorization header should contain your bearer token.

String fetchPresignedURL(PlatformFile file) async  {
    // Prepare headers for the API request, including the authorization token
    final headers = {
        'method': 'POST',
        'headers': {'Authorizaton': 'Bearer your-token'},
    };
    
    // Construct the request body with file information
    final body = {
       fileName: file.name,
       fileType: file.extension
    };

    // Send a POST request to the Sariska API endpoint
    final response = await http.post("https://api.sariska.io/api/v1/misc/get-presigned", headers, jsonEncode(body));
    // Handle successful response
    if (response.statusCode == 200) {
        var body = jsonDecode(response.body);
        return body.presignedUrl;
    // Handle unsuccessful responses
    } else {
        throw Exception('Failed to load album');
    }
}

Upload the File

After obtaining the presigned URL, the file can be uploaded to the URL using the PUT method.

void UploadFile() async {
    FilePickerResult file = await FilePicker.platform.pickFiles();
    // Fetch presigned URL from server
    String signedUrl  = await fetchPresignedURL(file);
    // Set headers for the PUT request
    final headers = {
       "ACL":"public-read",
       "Content-Disposition": "attachment"
    };
    // Send PUT request with file to presigned URL
    final response = await http.put(signedUrl, headers, file);
    // Handle successful response
    if (response.statusCode == 200) {
        var body = jsonDecode(response.body);
        return body.presignedUrl;
    // Handle unsuccessful responses
    } else {
        throw Exception('Failed to load album');
    }
}
await UploadFile(); // Call this to initiate the file upload process

Use presignedUrl to upload your files

Chat History

Retrieve the chat history using two methods:

  1. By Subscribing to Events

  • Subscribe to the archived_message event to receive the last 1000 messages in reverse chronological order.

void onArchivedMessage(dynamic payload) {
  if (payload["messages"] is Map<String, dynamic>) {
    Map<String, dynamic> messages = payload["messages"];
    // Process the received messages
    print("Received archived messages: $messages");
  }
}

Example usage demonstration
channel.on("archived_message", onArchivedMessage);

  • Subscribe to the archived_message_count event to get the total number of messages in the chat history.

void onArchivedMessageCount(dynamic payload) {
  if (payload["page"] is Map<String, dynamic>) {
    Map<String, dynamic> page = payload["page"];
    
    if (page["count"] is int) {
      int count = page["count"];
      // Process the received message count
      print("Total message count: $count");
    }
  }
}

Example usage demonstration
channel.on("archived_message_count", onArchivedMessageCount);

  • To retrieve a list of messages in the chat history, trigger the archived_message event to obtain the messages. Specify the size parameter to determine the number of messages you wish to fetch, and set the offset parameter as the starting index of the messages and group_by_day to group messages by day.

void pushArchivedMessage() {
  Map<String, dynamic> payload = {
    "page": {"offset": 20, "size": 20, "group_by_day": false},
  };
  channel.push("archived_message", payload);
}

Example usage demonstration
pushArchivedMessage();

  • To receive the total count of messages at any given time, initiate the archived_message_count trigger and subscribe to the corresponding event by listening for archived_message_count.

void pushArchivedMessageCount() {
  channel.push("archived_message_count", {});
}

Example usage demonstration
pushArchivedMessageCount();

  1. Using the Messages API

Make a GET request to the API endpoint to fetch the chat history for a specific room.

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> fetchChatHistory() async {
  final String url = 'https://api.sariska.io/api/v1/messaging/rooms/{room_name}/messages';

  try {
    final response = await http.get(
      Uri.parse(url),
      headers: {'Authorization': 'Bearer your-token'},
    );

    if (response.statusCode == 200) {
      final Map<String, dynamic> json = jsonDecode(response.body);
      print('Chat history: $json');
    } else {
      final errorMessage = 'Failed with status code ${response.statusCode}';
      throw Exception(errorMessage);
    }
  } catch (error) {
    print('Error fetching chat history: $error');
  }
}

Example usage demonstration
fetchChatHistory();

Fetch Specific Message

Retrieve any specific message from a room. It takes the room ID and message ID as parameters and sends a GET request to the Sariska API to fetch the specified message.

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> fetchSpecificMessage(String messageID) async {
  final String url = 'https://api.sariska.io/api/v1/messaging/rooms/{room_name}/messages/$messageID';

  try {
    final response = await http.get(
      Uri.parse(url),
      headers: {'Authorization': 'Bearer your-token'},
    );

    if (response.statusCode == 200) {
      final Map<String, dynamic> json = jsonDecode(response.body);
      print('Specific message: $json');
    } else {
      final errorMessage = 'Failed with status code ${response.statusCode}';
      throw Exception(errorMessage);
    }
  } catch (error) {
    print('Error fetching specific message: $error');
  }
}

Example usage demonstration
String messageId = 'your-message-id';
fetchSpecificMessage(messageId);

Delete Chat History

Delete chat history for a specific room.

Delete Single or Multiple Messages

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> emptyChat(List<String> messageIDs, String roomName) async {
  final String url = 'https://api.sariska.io/api/v1/messaging/rooms/$roomName/messages';

  try {
    final Map<String, dynamic> payload = {
      'method': 'DELETE',
      'headers': {'Authorization': 'Bearer your-token'},
      'body': {'message_ids': messageIDs, 'is_empty': false}
    };

    final response = await http.delete(
      Uri.parse(url),
      headers: {'Authorization': 'Bearer your-token'},
      body: jsonEncode(payload),
    );

    if (response.statusCode == 200) {
      final Map<String, dynamic> json = jsonDecode(response.body);
      print('Empty chat response: $json');
    } else {
      final errorMessage = 'Failed with status code ${response.statusCode}';
      throw Exception(errorMessage);
    }
  } catch (error) {
    print('Error emptying chat: $error');
  }
}

Example usage demonstration
List<String> messageIDs = ['message_id_1', 'message_id_2'];
String roomName = 'your-room-name';
emptyChat(messageIDs, roomName);

Delete All Chats

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> emptyChat(String roomName) async {
  final String url = 'https://api.sariska.io/api/v1/messaging/rooms/$roomName/messages';

  try {
    final Map<String, dynamic> payload = {
      'method': 'DELETE',
      'headers': {'Authorization': 'Bearer your-token'},
      'body': {'message_ids': [], 'is_empty': true}
    };

    final response = await http.delete(
      Uri.parse(url),
      headers: {'Authorization': 'Bearer your-token'},
      body: jsonEncode(payload),
    );

    if (response.statusCode == 200) {
      final Map<String, dynamic> json = jsonDecode(response.body);
      print('Empty chat response: $json');
    } else {
      final errorMessage = 'Failed with status code ${response.statusCode}';
      throw Exception(errorMessage);
    }
  } catch (error) {
    print('Error emptying chat: $error');
  }
}

Example usage demonstration
String roomName = 'your-room-name';
emptyChat(roomName);

Presence

The Presence object facilitates real-time synchronization of presence information between the server and the client, enabling the detection of user join and leave events.

Create a Presence Instance

To establish presence synchronization, instantiate a Presence object and provide the channel to track presence lifecycle events:

var channel = socket.channel("chat:room123");
var presence = Presence(channel);

State Synchronization

Utilize the presence.onSync callback to respond to state changes initiated by the server. For instance, to dynamically render the user list upon every list modification, implement the following:

presence.onSync(() => {
  myRenderUsersFunction(presence.list());
})

Handle Individual Join and Leave Events

The presence.onJoin and presence.onLeave callbacks allow for handling individual user join and leave events. Here is an instance:

var presence = new Presence(channel)

// Detect if the user has joined for the first time or from another tab/device
presence.onJoin((id, current, newPres) => {
  if(!current){
    print("user has entered for the first time", newPres)
  } else {
    print("user additional presence", newPres)
  }
})

// Detect if the user has left all tabs/devices, or is still present
presence.onLeave((id, current, leftPres) => {
  if(current.metas.length === 0){
    print("user has left from all devices", leftPres)
  } else {
    print("user left from a device", leftPres)
  }
})
// Receive presence data from server
presence.onSync(() => {
  print(presence.list()) // List of users 
})

Retrieve Presence Information

The presence.list(by:) method retrieves a list of presence information based on the local metadata state. By default, it returns the entire presence metadata. Alternatively, a listBy function can be provided to filter the metadata for each presence.

For instance, if a user is online from multiple devices, each with a metadata status of "online," but their status is set to "away" on another device, the application might prefer to display the "away" status in the UI.

The following example defines a listBy function that prioritizes the first metadata registered for each user, representing the first tab or device they used to connect:

var listBy = (id, {metas: [first, ...rest]}) => {
  // Increment the presence count for the user
  first.count = rest.length + 1
  // Set the user ID for the presence metadata
  first.id = id
  // Return the prioritized presence metadata
  return first
}
// Retrieve a list of online users based on the prioritized presence metadata
var onlineUsers = presence.list(listBy)

User Typing

Send information about a user who is typing. This information can be used to update the chat interface, such as displaying a "user is typing" indicator next to the user's name.

Send User Typing Event

When a user starts typing, the following code sends a typing event to other peers.

var payload = {
  "typing": true
};

channel
    .push("user_typing", payload)
    .receive("ok", (response) {
      print("Sent: $response");
    })
    .receive("error", (response) {
      print("Error: $response");
    });

Receive User Typing Event

Other peers in the chat can listen for the typing event on the same channel.

channel.on("user_typing", (message)=>{
    print(message)
});

For detailed real-time messaging API's, refer to Phoenix documentation.

For detailed management of chat and room APIs, refer to Sariska Swagger documentation.

Last updated