easy_chatview 0.2.7
easy_chatview: ^0.2.7 copied to clipboard
A package that lets you implement chatting the way you like it.
example/lib/main.dart
import 'dart:async';
import 'dart:math';
import 'package:easy_chatview/easy_chatview.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:utc_date_time/utc_date_time.dart';
void main() {
runApp(MyApp());
}
// I declare it here in the example, in your app you should not do that
// but use a different way of keeping only one single-source-of-truth
// datasource at a time.
final datasource = TestChatviewDataSource();
final chatController = TestChatviewController(
myUuid: '1',
datasource: datasource,
onSendTap: (
final context,
final message, {
required final chat,
}) async {
if (kDebugMode) {
print('sending ${message.content} in ${chat.uuid}');
print('marking as sent in 3 seconds');
}
await Future<void>.delayed(const Duration(seconds: 3));
if (kDebugMode) {
print('marking ${message.content} as sent');
}
message.uuid = '123-${message.sentAt.millisecondsSinceEpoch}';
},
);
final ValueNotifier<ThemeMode> themeModeChanger =
ValueNotifier<ThemeMode>(ThemeMode.light);
void changeTheme() {
themeModeChanger.value = themeModeChanger.value == ThemeMode.light
? ThemeMode.dark
: ThemeMode.light;
}
final defaultAppBarActions = <Widget>[
ValueListenableBuilder(
valueListenable: themeModeChanger,
builder: (final context, final themeMode, final _) => IconButton(
onPressed: changeTheme,
icon: Icon(
themeMode == ThemeMode.light
? Icons.dark_mode_outlined
: Icons.light_mode,
),
),
),
];
class MyApp extends StatelessWidget {
MyApp({super.key}) {
// unawaited(_startChangingTheme());
}
// ignore: unused_element
Future<void> _startChangingTheme() async {
while (true) {
await Future<void>.delayed(const Duration(seconds: 5));
changeTheme();
}
}
@override
Widget build(final BuildContext context) {
const colorSchemeSeedColor = Colors.blue;
return ValueListenableBuilder(
valueListenable: themeModeChanger,
builder: (final context, final themeMode, final _) => MaterialApp(
themeMode: themeMode,
theme: ThemeData(
colorSchemeSeed: colorSchemeSeedColor,
brightness: Brightness.light,
),
darkTheme: ThemeData(
colorSchemeSeed: colorSchemeSeedColor,
brightness: Brightness.dark,
),
title: 'My First App',
home: Scaffold(
appBar: AppBar(
title: const Text('Chat list'),
actions: defaultAppBarActions,
),
body: Center(
child: TestChatviewChatListView(
chatviewController: chatController,
),
),
),
),
);
}
}
class TestChatviewChatListView extends EasyChatviewChatListView<
TestChatviewMessage, TestChatviewChat, TestChatviewUser> {
TestChatviewChatListView({
required super.chatviewController,
super.key,
});
@override
Widget chatListItemBuilder(
final BuildContext context,
final TestChatviewChat chat,
) =>
ValueListenableBuilder(
valueListenable: chat,
builder: (final context, final _, final __) => ListTile(
onTap: () => _onTapChat(
context,
chat,
),
title: Text(chat.title),
subtitle: Text(chat.messagePreview),
),
);
void _onTapChat(final BuildContext context, final TestChatviewChat chat) {
unawaited(
Navigator.of(context).push(
MaterialPageRoute(
builder: (final context) => Scaffold(
appBar: AppBar(
title: Text('Chat: ${chat.title}'),
actions: defaultAppBarActions,
),
body: Center(
child: TestChatviewChatView(
chat: chat,
chatviewController: chatviewController,
),
),
),
),
),
);
}
}
class TestChatviewChatView extends EasyChatviewChatView<TestChatviewMessage,
TestChatviewChat, TestChatviewUser> {
TestChatviewChatView({
required super.chatviewController,
required super.chat,
super.key,
});
@override
List<Color>? getTheirColorGradient(final BuildContext context) => [
Colors.teal,
Colors.purpleAccent,
];
@override
List<Color>? getMyColorGradient(final BuildContext context) => [
Colors.orange,
];
Widget _replyToHeadingBuilder(final TestChatviewMessage inReplyToMessage) =>
Text('in reply to: ${inReplyToMessage.content}');
@override
Widget chatMessageBuilder(
final BuildContext context,
final EasyChatviewMessageBuilderInfo<TestChatviewMessage> info,
) {
if (info.message.isSystemMessage) {
return ListTile(
title: Text('system: ${info.message.sentAt}'),
subtitle: Text(info.message.content),
);
}
Widget replyToWidget = const SizedBox.shrink();
if (info.message.inReplyTo != null) {
final replyToTuple = chatviewController.getMessageByUuid(
info.message.inReplyTo!,
chat: chat,
);
if (replyToTuple.isLoading) {
replyToWidget = FutureBuilder(
future: replyToTuple.reloader,
builder: (final context, final snapshot) {
if (snapshot.hasData) {
return _replyToHeadingBuilder(snapshot.data!);
}
if (snapshot.hasError) {
return Text('error: ${snapshot.error}');
}
return const CircularProgressIndicator();
},
);
} else {
replyToWidget = _replyToHeadingBuilder(replyToTuple.item!);
}
}
return Card.filled(
margin: const EdgeInsets.all(8),
color: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
replyToWidget,
Text(
'š¼ ${info.prevMessage?.content}'
'\n text: ${info.message.content}'
'\nš½ ${info.nextMessage?.content}',
textAlign: TextAlign.start,
),
],
),
// subtitle: Text(
// info.message.sentAt.todmYHM(),
// style: const TextStyle(
// color: Colors.black,
// ),
// ),
);
}
}
class TestChatviewController extends EasyChatviewController<TestChatviewMessage,
TestChatviewChat, TestChatviewUser> {
TestChatviewController({
required super.myUuid,
required this.datasource,
required this.onSendTap,
}) {
// unawaited(_startChangingUserUuid());
// unawaited(_startChangingReplyTo());
}
// ignore: unused_element
Future<void> _startChangingReplyTo() async {
while (true) {
await Future<void>.delayed(const Duration(milliseconds: 5000));
_setRandomReplyTo();
}
}
void _setRandomReplyTo() {
if (hasReplyInitiated) {
cancelReply();
return;
}
initiateReply(
TestChatviewMessage(
uuid: 'aab',
senderUuid: '1',
sentAt: UtcDateTime.now(),
content: '132132132123',
),
);
}
// ignore: unused_element
Future<void> _startChangingUserUuid() async {
while (true) {
await Future<void>.delayed(const Duration(milliseconds: 5000));
_setRandomUuid();
}
}
void _setRandomUuid() {
final allUsers = datasource
.getUsers(
chat: datasource.getChats().item.first,
)
.item;
final newUser = allUsers.elementAt(Random().nextInt(allUsers.length));
if (kDebugMode) {
print('setting myUuid to ${newUser.uuid}');
}
myUuid = newUser.uuid;
}
@override
final TestChatviewDataSource datasource;
final Future<void> Function(
BuildContext context,
TestChatviewMessage message, {
required TestChatviewChat chat,
}) onSendTap;
@override
TestChatviewMessage newUserMessageFromText(
final String text, {
required final TestChatviewMessage? inReplyToMessage,
}) =>
TestChatviewMessage(
uuid: null,
sentAt: UtcDateTime.now(),
content: text,
senderUuid: myUuid,
inReplyTo: inReplyToMessage?.uuid,
);
@override
Future<void> userSentTextMessage(
final BuildContext context,
final TestChatviewMessage message, {
required final String? inReplyTo,
required final TestChatviewChat chat,
}) =>
onSendTap(context, message, chat: chat);
}
class TestChatviewDataSource extends EasyChatviewDataSource<TestChatviewMessage,
TestChatviewChat, TestChatviewUser> {
TestChatviewDataSource() : super() {
while (_addCounter < 10) {
_addRandomChangingMessage();
}
unawaited(_startAddingRandomMessage());
}
int _addCounter = 0;
int _dayAdder = 0;
int _monthAdder = 0;
final List<String> _possibleSenders = [
'1',
'2',
'3',
'4',
];
final int messageDeltaMillis = 200;
// final int messageDeltaMillis = 0;
Future<void> _startAddingRandomMessage() async {
while (_addCounter < 1000) {
await Future<void>.delayed(
Duration(
milliseconds: messageDeltaMillis,
),
);
_addRandomChangingMessage();
}
}
void _addRandomChangingMessage() {
if (Random().nextInt(100) > 50) {
_dayAdder += 1;
}
if (Random().nextInt(100) > 90) {
_monthAdder += 1;
}
final realSentAt = UtcDateTime.now().subtract(
_addCounter.isEven
? Duration(
milliseconds: (messageDeltaMillis * 1.5) ~/ 1,
)
: Duration.zero,
);
final sentAt = realSentAt.add(Duration(days: _dayAdder)).copyWith(
month: 1 + (realSentAt.month + _monthAdder) % 12,
year: 1 + realSentAt.year + ((realSentAt.month + _monthAdder) ~/ 12),
);
final newMessage = TestChatviewMessage(
uuid: Random().nextBool()
? UtcDateTime.now().millisecondsSinceEpoch.toString()
: null,
allHaveReceived: Random().nextBool(),
allHaveRead: Random().nextBool(),
sentAt: sentAt,
content: '$_addCounter: $sentAt',
senderUuid: _possibleSenders[Random().nextInt(_possibleSenders.length)],
);
// final chatUuid =
// _testMessages.keys.elementAt(_addCounter % _testMessages.length);
final chatUuid = _testMessages.keys.first;
if (kDebugMode) {
print(
'adding message ${newMessage.sentAt.todmYHMS()} '
'-> ${_testMessages[chatUuid]!.messages.length}',
);
}
_testMessages[chatUuid]!.addMessage(newMessage);
unawaited(
Future<void>.delayed(const Duration(milliseconds: 1000)).then(
(final _) => newMessage.content =
'${newMessage.content}: ${UtcDateTime.now().todmYHMS()}',
),
);
_addCounter++;
}
final _testMessages = {
'abc': TestChatviewChat(
uuid: 'abc',
title: 'Testchat abc :)',
messages: [
TestChatviewMessage.system(
uuid: 'system1',
sentAt: UtcDateTime.now().subtract(
const Duration(minutes: 5),
),
content: 'Testmessage 1 in abc',
visibleToAll: true,
),
TestChatviewMessage(
uuid: 'userabc1',
senderUuid: 'x123x',
sentAt: UtcDateTime.now().subtract(
const Duration(minutes: 3),
),
content: 'Usermessage 1 in abc',
),
TestChatviewMessage(
uuid: 'userabc2',
senderUuid: 'x123x',
sentAt: UtcDateTime.now().subtract(
const Duration(minutes: 1),
),
content: 'Testmessage 2 in abc',
),
],
),
'def': TestChatviewChat(
uuid: 'def',
title: 'Testchat def :o',
messages: [
TestChatviewMessage.video(
uuid: 'userdef1',
senderUuid: 'x123x',
sentAt: UtcDateTime.now().subtract(
const Duration(minutes: 5),
),
videoName: 'MyVideo',
),
TestChatviewMessage(
uuid: 'userdef2',
senderUuid: 'x123x',
sentAt: UtcDateTime.now().subtract(
const Duration(minutes: 3),
),
content: 'Testmessage 2 in def',
),
TestChatviewMessage(
uuid: 'userdef2',
senderUuid: '1',
sentAt: UtcDateTime.now().subtract(
const Duration(minutes: 3),
),
content: 'Testmessage 3 in def',
),
],
),
};
TestChatviewUser _makeUserFromUuid(
final String uuid,
) =>
TestChatviewUser(
uuid: uuid,
displayName: 'user $uuid',
);
@override
FutureOr<Iterable<TestChatviewUser>> loadUsers({
required final TestChatviewChat chat,
}) =>
_possibleSenders.map(_makeUserFromUuid);
@override
FutureOr<TestChatviewUser> loadUser({
required final String uuid,
required final TestChatviewChat chat,
}) =>
Future<void>.delayed(const Duration(seconds: 2))
.then((final _) => _makeUserFromUuid(uuid));
@override
FutureOr<Iterable<TestChatviewChat>> loadChats() => _testMessages.values;
@override
FutureOr<Iterable<TestChatviewMessage>> loadMessages({
required final TestChatviewChat chat,
}) {
if (!_testMessages.containsKey(chat.uuid)) {
return [];
}
return _testMessages[chat.uuid]!.messageList;
}
@override
FutureOr<TestChatviewMessage> loadMessage({
required final String uuid,
required final TestChatviewChat chat,
}) async {
await Future<void>.delayed(const Duration(seconds: 5));
if (!_testMessages.containsKey(chat.uuid)) {
return Future.error(Exception('not found'));
}
return _testMessages[chat.uuid]!
.messageList
.firstWhere((final msg) => msg.uuid == uuid);
}
}
class TestChatviewMessage extends EasyChatviewMessage<String> {
TestChatviewMessage({
required super.uuid,
required super.sentAt,
required super.content,
required super.senderUuid,
super.allHaveReceived,
super.allHaveRead,
super.inReplyTo,
super.error,
}) : super.user();
TestChatviewMessage.system({
required super.uuid,
required super.sentAt,
required super.content,
required super.visibleToAll,
}) : super.system();
TestChatviewMessage.video({
required super.uuid,
required super.sentAt,
required super.senderUuid,
required final String videoName,
super.allHaveReceived,
super.allHaveRead,
super.inReplyTo,
super.error,
}) : super.user(content: 'video: $videoName');
}
class TestChatviewChat
extends EasyChatviewChat<TestChatviewMessage, TestChatviewUser> {
TestChatviewChat({
required super.uuid,
required super.title,
super.messages,
});
}
class TestChatviewUser extends EasyChatviewUser {
TestChatviewUser({
required super.uuid,
required super.displayName,
});
}