Skip to content

Mobile (Flutter)

A minimal client-side flow for messaging in a Flutter app. This mirrors the web clients (same GraphQL API, same WebSocket protocol, same token), it's just a different client on the same backend described in Implementation. Snippets below are intentionally minimal (illustrative, not production-ready). Treat exact APIs as dependent on whichever GraphQL/WebSocket package version you land on.

What's Needed

  • A GraphQL HTTP client for queries/mutations (createConversation, sendMessage, moderateConversation, ...)
  • A WebSocket client for subscriptions (messageAdded, notificationReceived), using the same bearer token already obtained from sign-in
  • A multipart HTTP upload for attachments, ahead of sending a message that references them

Reuse whatever graphql/graphql_flutter-style package the rest of the app already uses for HTTP; the only messaging-specific piece is the subscription handshake.

Subscribing to a Conversation

The token goes in the connection_init payload, not a header, the same handshake described in Implementation:

final channel = WebSocketChannel.connect(
  Uri.parse('wss://api.travo-mate.online/cargo/graphql'),
  protocols: ['graphql-transport-ws'],
);

channel.sink.add(jsonEncode({
  'type': 'connection_init',
  'payload': {'authorization': 'Bearer $jwt'},
}));

channel.stream.listen((raw) {
  final msg = jsonDecode(raw);
  if (msg['type'] == 'connection_ack') {
    channel.sink.add(jsonEncode({
      'id': '1',
      'type': 'subscribe',
      'payload': {
        'query': 'subscription(\$id: ID!) { messageAdded(conversationId: \$id) { id text senderId } }',
        'variables': {'id': conversationId},
      },
    }));
  } else if (msg['type'] == 'next') {
    onNewMessage(msg['payload']['data']['messageAdded']);
  }
});

Sending a Message

A plain GraphQL mutation over HTTP, like any other write in the app:

await graphQLClient.mutate(MutationOptions(
  document: gql(r'''
    mutation($id: ID!, $text: String, $attachmentIds: [ID!]) {
      sendMessage(conversationId: $id, text: $text, attachmentIds: $attachmentIds) { id }
    }
  '''),
  variables: {'id': conversationId, 'text': text, 'attachmentIds': attachmentIds},
));

Uploading an Attachment

Upload first, then include the returned id on sendMessage:

final request = http.MultipartRequest('POST', Uri.parse('https://api.travo-mate.online/cargo/media'))
  ..headers['Authorization'] = 'Bearer $jwt'
  ..files.add(await http.MultipartFile.fromPath('file', filePath));

final response = await request.send();
final body = jsonDecode(await response.stream.bytesToString());
final attachmentId = body['id'];

Render an image attachment with the token as a query parameter, the same fallback the web clients use since a request for image bytes can't carry a custom header:

Image.network('https://api.travo-mate.online/cargo/media/$attachmentId?token=$jwt')

Notifications

Same connection, same handshake, a second subscription. Most apps open this once at app-start rather than per-screen:

channel.sink.add(jsonEncode({
  'id': '2',
  'type': 'subscribe',
  'payload': {
    'query': 'subscription { notificationReceived { id type text relatedConversationId } }',
  },
}));

Use relatedConversationId to jump straight to the right conversation when a notification is tapped.

Not Mobile-Specific

Everything else (permissions, conversation/message shape, moderation actions, what triggers a notification) is identical to the web clients and covered in Implementation and Mediation & Flagging. There's nothing mobile-only in the messaging model itself.