noverachat_flutter 0.5.3 copy "noverachat_flutter: ^0.5.3" to clipboard
noverachat_flutter: ^0.5.3 copied to clipboard

Flutter bindings for NoveraChat — lifecycle-managed providers and controllers over the headless noverachat_dart core.

example/lib/main.dart

// Minimal example wiring the noverachat_flutter bindings into one chat screen.
//
// Configuration is read at compile time so no secret lives in the source. Run:
//
//   flutter run \
//     --dart-define=NOVERACHAT_ENDPOINT=https://chat.example.com \
//     --dart-define=NOVERACHAT_APP_ID=app_9f8k2x \
//     --dart-define=NOVERACHAT_TOKEN=<a per-user JWT from your backend> \
//     --dart-define=NOVERACHAT_ROOM=room_123
//
// In a real app, `tokenProvider` fetches a fresh JWT from YOUR backend; here we
// read one from a --dart-define just to keep the example runnable.

import 'dart:convert';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:noverachat_flutter/noverachat_flutter.dart';

const _endpoint =
    String.fromEnvironment('NOVERACHAT_ENDPOINT', defaultValue: 'https://chat.example.com');
const _appId =
    String.fromEnvironment('NOVERACHAT_APP_ID', defaultValue: 'app_9f8k2x');
const _token = String.fromEnvironment('NOVERACHAT_TOKEN', defaultValue: '');
const _roomId =
    String.fromEnvironment('NOVERACHAT_ROOM', defaultValue: 'room_123');

void main() => runApp(const ExampleApp());

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'noverachat_flutter example',
      // Provide a connected NoveraChat to the whole app.
      home: NoveraChatScope(
        options: ClientOptions(
          appId: _appId,
          endpoint: _endpoint,
          tokenProvider: () async => _token,
        ),
        child: const ChatScreen(roomId: _roomId),
      ),
    );
  }
}

class ChatScreen extends StatefulWidget {
  const ChatScreen({super.key, required this.roomId});

  final String roomId;

  @override
  State<ChatScreen> createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  // Who "I" am — used only for the reaction toggle demo. In a real app this
  // comes from your auth layer (the user the JWT was issued for).
  static const _myUserId =
      String.fromEnvironment('NOVERACHAT_USER', defaultValue: 'me');

  RoomController? _messages;
  TypingController? _typing;
  final _input = TextEditingController();
  ChatMessage? _replyTo;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // Build the controllers once, now that NoveraChatScope is above us.
    if (_messages == null) {
      final chat = NoveraChatScope.of(context);
      final room = chat.room(widget.roomId);
      _messages = RoomController(room)..loadHistory();
      _typing = TypingController(room);
    }
  }

  @override
  void dispose() {
    _messages?.dispose();
    _typing?.dispose();
    _input.dispose();
    super.dispose();
  }

  void _send() {
    final text = _input.text.trim();
    if (text.isEmpty) return;
    // Replying? The target id rides along and the banner clears.
    _messages!.send(text, replyToId: _replyTo?.id);
    setState(() => _replyTo = null);
    _input.clear();
  }

  // File-send demo without a picker dependency: upload a tiny generated
  // text file. Swap in file_picker/image_picker bytes in a real app.
  void _sendDemoFile() {
    final bytes = Uint8List.fromList(
      utf8.encode('hello from noverachat_flutter example '
          '${DateTime.now().toIso8601String()}'),
    );
    _messages!.sendFile(
      FileSource(bytes: bytes, mime: 'text/plain', name: 'demo.txt'),
    );
  }

  String _subtitle(ChatMessage m) {
    final parts = [
      m.senderId ?? 'me',
      m.status.name,
      if (m.messageType != 'TEXT') m.messageType,
      if (m.uploadProgress != null)
        'uploading ${(m.uploadProgress! * 100).round()}%',
      if (m.editedCount > 0) 'edited',
      if (m.replyToId != null) '↩ ${m.replyToId}',
      for (final r in m.reactions) '${r.key}×${r.userIds.length}',
    ];
    return parts.join(' · ');
  }

  @override
  Widget build(BuildContext context) {
    final messages = _messages!;
    return Scaffold(
      appBar: AppBar(title: Text('Room ${widget.roomId}')),
      body: Column(
        children: [
          Expanded(
            child: MessagesBuilder(
              controller: messages,
              builder: (context, list) => ListView.builder(
                // +1 for the scrollback button while older pages remain.
                itemCount: list.length + (messages.hasMore ? 1 : 0),
                itemBuilder: (context, i) {
                  if (messages.hasMore && i == 0) {
                    return TextButton(
                      onPressed: messages.loadMore,
                      child: const Text('load older messages'),
                    );
                  }
                  final m = list[i - (messages.hasMore ? 1 : 0)];
                  return ListTile(
                    dense: true,
                    title: Text(m.isDeleted
                        ? '(deleted)'
                        : m.messageType == 'FILE'
                            ? '📎 ${m.file?.name ?? m.fileId ?? 'file'}'
                            : (m.content ?? '')),
                    subtitle: Text(_subtitle(m)),
                    // Tap: toggle a ❤️ reaction. Long-press: reply to it.
                    onTap: m.status == ChatMessageStatus.sent
                        ? () => messages.toggleReaction(m.id, 'emoji_heart',
                            myUserId: _myUserId)
                        : null,
                    onLongPress: () => setState(() => _replyTo = m),
                  );
                },
              ),
            ),
          ),
          if (_replyTo != null)
            ListTile(
              dense: true,
              leading: const Icon(Icons.reply, size: 16),
              title: Text(
                'replying to: ${_replyTo!.content ?? _replyTo!.id}',
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
              trailing: IconButton(
                icon: const Icon(Icons.close, size: 16),
                onPressed: () => setState(() => _replyTo = null),
              ),
            ),
          // "… is typing" indicator.
          ListenableBuilder(
            listenable: _typing!,
            builder: (context, _) => _typing!.isAnyoneTyping
                ? const Align(
                    alignment: Alignment.centerLeft,
                    child: Padding(
                      padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
                      child: Text('typing…', style: TextStyle(fontStyle: FontStyle.italic)),
                    ),
                  )
                : const SizedBox.shrink(),
          ),
          SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(8),
              child: Row(
                children: [
                  IconButton(
                    icon: const Icon(Icons.attach_file),
                    onPressed: _sendDemoFile,
                  ),
                  Expanded(
                    child: TextField(
                      controller: _input,
                      onChanged: (_) =>
                          NoveraChatScope.of(context).room(widget.roomId).setTyping(true),
                      onSubmitted: (_) => _send(),
                      decoration: const InputDecoration(hintText: 'Message'),
                    ),
                  ),
                  IconButton(icon: const Icon(Icons.send), onPressed: _send),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}
0
likes
0
points
431
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter bindings for NoveraChat — lifecycle-managed providers and controllers over the headless noverachat_dart core.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, noverachat_dart

More

Packages that depend on noverachat_flutter