flutter_frappe_chat 1.1.0 copy "flutter_frappe_chat: ^1.1.0" to clipboard
flutter_frappe_chat: ^1.1.0 copied to clipboard

Frappe Chat for Flutter. Realtime messaging over Socket.IO, attachments, voice notes, typing indicators and a themeable chat screen for Frappe/ERPNext sites.

flutter_frappe_chat #

Frappe Chat for Flutter: realtime messaging over Socket.IO, attachments, voice notes, typing indicators and a themeable chat screen.

pub package

Requires the frappe/chat app installed on your Frappe / ERPNext site.

final config = FrappeChatConfig(
  baseUrl: 'https://erp.example.com',
  sid: mySessionId,
);

Navigator.push(context, MaterialPageRoute(
  builder: (_) => ChatScreen(
    config: config,
    room: 'ROOM-0001',
    sender: 'Jane Doe',
    senderEmail: 'jane@example.com',
    chatPartnerName: 'Support',
    theme: FrappeChatTheme.fromTheme(Theme.of(context)),
  ),
));

Features #

  • Realtime messages over Frappe's Socket.IO service
  • Typing indicators
  • Images, PDFs and arbitrary file attachments
  • Voice notes with a live waveform
  • A themeable ChatScreen, or build your own on FrappeApiService and FrappeSocketManager
  • Token or session authentication
  • Private attachments, fetched with credentials

Platforms #

Android, iOS and desktop. Not web — the package uses dart:io for file handling and audio recording.

Install #

dependencies:
  flutter_frappe_chat: ^1.1.0

Requires Dart 3.8 / Flutter 3.32 or newer.

Android #

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<!-- Android 12 and below -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32"/>

minSdkVersion 24 or higher, for flutter_sound.

iOS #

<key>NSMicrophoneUsageDescription</key>
<string>Record voice notes</string>
<key>NSCameraUsageDescription</key>
<string>Take photos to send in chat</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Attach photos to messages</string>

Configuration #

final config = FrappeChatConfig(
  baseUrl: 'https://erp.example.com',

  // Either token auth...
  apiKey: 'your_api_key',
  apiSecret: 'your_api_secret',

  // ...or a session
  sid: mySessionId,

  timeout: const Duration(seconds: 30),
  privateAttachments: true,
);

With flutter_next_auth:

final config = FrappeChatConfig(
  baseUrl: 'https://erp.example.com',
  sid: await flutternext.getStoredSid(),
);

The socket URL #

Frappe's realtime server registers its handlers on io.of(/^\/.*$/) and then rejects any namespace that is not the site name. Connecting to the bare host fails with Invalid namespace.

This package appends the namespace for you — socketUrl becomes https://erp.example.com/erp.example.com. Set siteName when the site name differs from the host:

FrappeChatConfig(
  baseUrl: 'http://localhost:8000',
  siteName: 'mysite.localhost',   // -> http://localhost:8000/mysite.localhost
);

Frappe also refuses a connection whose Origin header does not match Host. Native Dart clients send no Origin, so this package sends one derived from baseUrl, along with x-frappe-site-name.

Theming #

ChatScreen(
  // ...
  theme: FrappeChatTheme.fromTheme(Theme.of(context)),
);

Or set colours yourself:

const FrappeChatTheme(
  background: Color(0xFF0E1621),
  outgoingBubble: Color(0xFF2B5278),
  incomingBubble: Color(0xFF182533),
  outgoingText: Colors.white,
  incomingText: Colors.white,
  accent: Color(0xFF64B5F6),
  bubbleRadius: 16,
);

Building your own UI #

final api = FrappeApiService(config);
final socket = FrappeSocketManager(config);

socket
  ..onMessageReceived = (message) => setState(() => _messages.add(message))
  ..onTypingChanged = (isTyping, user) => setState(() => _typing = isTyping)
  ..onChatUpdate = (update) => refreshRoomList()
  ..connect(room: roomId);

// Watch the real connection state — never assume connect() succeeded.
socket.connectionStatus.addListener(() {
  final status = socket.connectionStatus.value;
  if (status.state == ChatConnectionState.failed) {
    print('Realtime failed: ${status.message}');
  }
});

await api.sendMessage(roomId, 'Hello', 'Jane Doe', 'jane@example.com');
await api.setTyping(roomId, 'Jane Doe', true);
await api.markRoomAsRead(roomId);

socket.dispose();
api.dispose();

Identifying messages #

chat.api.message.get_all returns only content, sender, creation and sender_emailno document id — and the realtime payload carries none either. Use ChatMessage.localKey, which derives a stable identity from those fields, for deduplication and list keys. ChatMessage implements == and hashCode on it, so a Set<ChatMessage> deduplicates correctly.

Attachments #

final url = await api.uploadFileBytes(
  fileName: 'photo.png',
  bytes: bytes,
);
await api.sendMessage(roomId, url, sender, senderEmail);

Frappe serves /files/ to anyone with the URL, with no authentication. Set privateAttachments: true for confidential chats — attachments then land under /private/files/ and this package fetches them with credentials. Private audio is downloaded to a temporary file before playback, because flutter_sound cannot send headers.

Pagination #

The stock chat.api.message.get_all takes no pagination arguments and returns the entire room history on every open. getMessages() sends limit_start and limit_page_length anyway, so overriding the method in a custom app is all that is needed:

@frappe.whitelist()
def get_all(room, email, limit_start=0, limit_page_length=50):
    if not is_user_allowed_in_room(room, email):
        raise_not_authorized_error()
    return frappe.get_all(
        "Chat Message",
        filters={"room": room},
        fields=["name", "content", "sender", "creation", "sender_email"],
        order_by="creation desc",
        limit_start=int(limit_start),
        limit_page_length=int(limit_page_length),
    )

Adding name to the field list also gives messages real ids.

Read receipts #

Off by default. The stock Chat Message DocType has no seen field, so a tick could never change state. Add one in a custom app, then turn the UI on:

ChatScreen(showReadReceipts: true, /* ... */);

Troubleshooting #

Symptom Cause
Invalid namespace The socket URL is missing the site namespace. Set siteName.
Invalid origin Frappe compares Origin to Host. Check that baseUrl is the host actually serving the site.
Unauthorized Bad sid or API key/secret.
Connects, no messages Confirm the sender's email is a member of the room — chat.api.message.send publishes only to members.
Attachments 404 Private files need credentials. Set privateAttachments consistently.

FrappeSocketManager logs a specific hint for each of the first three.

API reference #

Type Purpose
FrappeChatConfig URL, credentials, socket namespace, timeouts
FrappeApiService getMessages, sendMessage, setTyping, markRoomAsRead, uploadFileBytes
FrappeSocketManager connect, subscribeToRoom, connectionStatus, dispose
ChatScreen The ready-made screen
FrappeChatTheme Colours and shapes
ChatMessage localKey, createdAt, isFrom
MessageBubble A single bubble, with ChatAttachmentKind detection
ChatPermissions Microphone, camera and media access

License #

MIT — see LICENSE.

1
likes
160
points
100
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Frappe Chat for Flutter. Realtime messaging over Socket.IO, attachments, voice notes, typing indicators and a themeable chat screen for Frappe/ERPNext sites.

Repository (GitHub)
View/report issues

Topics

#frappe #erpnext #chat #websocket #realtime

License

(pending) (license)

Dependencies

cached_network_image, file_picker, flutter, flutter_sound, http, image_picker, intl, path_provider, permission_handler, socket_io_client, url_launcher

More

Packages that depend on flutter_frappe_chat