noverachat_flutter 0.0.3
noverachat_flutter: ^0.0.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 '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> {
RoomController? _messages;
TypingController? _typing;
final _input = TextEditingController();
@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;
_messages!.send(text);
_input.clear();
}
@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(
itemCount: list.length,
itemBuilder: (context, i) {
final m = list[i];
return ListTile(
dense: true,
title: Text(m.isDeleted ? '(deleted)' : (m.content ?? '')),
subtitle: Text('${m.senderId ?? "me"} · ${m.status.name}'),
);
},
),
),
),
// "… 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: [
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),
],
),
),
),
],
),
);
}
}