Merge pull request #29 from Iconica-Development/0.6.0

feat: made message controller nullable
This commit is contained in:
Freek van de Ven 2023-12-13 17:26:12 +01:00 committed by Freek van de Ven
commit 7e503bb0ce
11 changed files with 296 additions and 217 deletions

View file

@ -1,3 +1,8 @@
## 0.6.0 - December 1 2023
- Made the message controller nullable
- Improved chat UI and added showTime option for chatDetailScreen to always show the time
## 0.5.0 - November 29 2023
- Added the option to add your own dialog on chat delete and addded the option to make the chat not deletable

View file

@ -4,7 +4,7 @@
name: flutter_community_chat
description: A new Flutter package project.
version: 0.5.0
version: 0.6.0
publish_to: none
@ -19,12 +19,12 @@ dependencies:
git:
url: https://github.com/Iconica-Development/flutter_community_chat
path: packages/flutter_community_chat_view
ref: 0.5.0
ref: 0.6.0
flutter_community_chat_interface:
git:
url: https://github.com/Iconica-Development/flutter_community_chat
path: packages/flutter_community_chat_interface
ref: 0.5.0
ref: 0.6.0
dev_dependencies:
flutter_lints: ^2.0.0

View file

@ -19,7 +19,7 @@ class FirebaseMessageService implements MessageService {
late final ChatUserService _userService;
late FirebaseChatOptions _options;
late StreamController<List<ChatMessageModel>> _controller;
StreamController<List<ChatMessageModel>>? _controller;
StreamSubscription<QuerySnapshot>? _subscription;
FirebaseMessageService({
@ -65,8 +65,12 @@ class FirebaseMessageService implements MessageService {
'last_message': message,
});
if (chat.id != null && _controller.hasListener && (_subscription == null)) {
_subscription = _startListeningForMessages(chat);
if (_controller != null) {
if (chat.id != null &&
_controller!.hasListener &&
(_subscription == null)) {
_subscription = _startListeningForMessages(chat);
}
}
// update the chat counter for the other users
@ -166,7 +170,7 @@ class FirebaseMessageService implements MessageService {
},
);
return _controller.stream;
return _controller!.stream;
}
StreamSubscription<QuerySnapshot> _startListeningForMessages(ChatModel chat) {
@ -204,7 +208,7 @@ class FirebaseMessageService implements MessageService {
}
}
_controller.add(messages);
_controller?.add(messages);
},
);
}

View file

@ -4,7 +4,7 @@
name: flutter_community_chat_firebase
description: A new Flutter package project.
version: 0.5.0
version: 0.6.0
publish_to: none
environment:
@ -23,7 +23,7 @@ dependencies:
git:
url: https://github.com/Iconica-Development/flutter_community_chat
path: packages/flutter_community_chat_interface
ref: 0.5.0
ref: 0.6.0
dev_dependencies:
flutter_lints: ^2.0.0

View file

@ -4,7 +4,7 @@
name: flutter_community_chat_interface
description: A new Flutter package project.
version: 0.5.0
version: 0.6.0
publish_to: none
environment:

View file

@ -75,6 +75,7 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
users: [pietUser, janUser],
lastUsed: DateTime.now().subtract(const Duration(days: 1)),
messages: messages,
canBeDeleted: false,
);
Stream<List<ChatModel>> get chatStream => (() {

View file

@ -15,10 +15,7 @@ dependencies:
flutter:
sdk: flutter
flutter_community_chat_view:
git:
url: https://github.com/Iconica-Development/flutter_community_chat
path: packages/flutter_community_chat_view
ref: 0.5.0
path: ..
dev_dependencies:
flutter_test:

View file

@ -11,16 +11,18 @@ import 'package:flutter_community_chat_view/src/services/date_formatter.dart';
class ChatDetailRow extends StatefulWidget {
const ChatDetailRow({
required this.translations,
required this.isFirstMessage,
required this.message,
required this.userAvatarBuilder,
this.previousMessage,
this.showTime = false,
super.key,
});
final ChatTranslations translations;
final bool isFirstMessage;
final ChatMessageModel message;
final UserAvatarBuilder userAvatarBuilder;
final bool showTime;
final ChatMessageModel? previousMessage;
@override
State<ChatDetailRow> createState() => _ChatDetailRowState();
@ -30,82 +32,119 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
final DateFormatter _dateFormatter = DateFormatter();
@override
Widget build(BuildContext context) => Padding(
padding: EdgeInsets.only(top: widget.isFirstMessage ? 25.0 : 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Opacity(
opacity: widget.isFirstMessage ? 1 : 0,
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: widget.message.sender.imageUrl != null &&
widget.message.sender.imageUrl!.isNotEmpty
? ChatImage(
image: widget.message.sender.imageUrl!,
)
: widget.userAvatarBuilder(
widget.message.sender,
30,
),
),
Widget build(BuildContext context) {
var isNewDate = widget.previousMessage != null &&
widget.message.timestamp.day != widget.previousMessage!.timestamp.day;
return Padding(
padding: EdgeInsets.only(
top: isNewDate ||
widget.previousMessage == null ||
widget.previousMessage?.sender.id != widget.message.sender.id
? 25.0
: 0,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isNewDate ||
widget.previousMessage == null ||
widget.previousMessage?.sender.id !=
widget.message.sender.id) ...[
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: widget.message.sender.imageUrl != null &&
widget.message.sender.imageUrl!.isNotEmpty
? ChatImage(
image: widget.message.sender.imageUrl!,
)
: widget.userAvatarBuilder(
widget.message.sender,
30,
),
),
Expanded(
child: Container(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (widget.isFirstMessage)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.message.sender.fullName?.toUpperCase() ??
widget.translations.anonymousUser,
] else ...[
const SizedBox(
width: 50,
),
],
Expanded(
child: Container(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (isNewDate ||
widget.previousMessage == null ||
widget.previousMessage?.sender.id !=
widget.message.sender.id)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.message.sender.fullName?.toUpperCase() ??
widget.translations.anonymousUser,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Text(
_dateFormatter.format(
date: widget.message.timestamp,
showFullDate: true,
),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
fontSize: 12,
color: Color(0xFFBBBBBB),
),
),
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Text(
_dateFormatter.format(
date: widget.message.timestamp,
showFullDate: true,
),
style: const TextStyle(
fontSize: 12,
color: Color(0xFFBBBBBB),
),
),
),
],
),
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: widget.message is ChatTextMessageModel
? Text(
(widget.message as ChatTextMessageModel).text,
style: const TextStyle(fontSize: 16),
overflow: TextOverflow.ellipsis,
maxLines: 999,
)
: CachedNetworkImage(
imageUrl:
(widget.message as ChatImageMessageModel)
.imageUrl,
),
),
],
),
],
),
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: widget.message is ChatTextMessageModel
? RichText(
text: TextSpan(
text: (widget.message as ChatTextMessageModel)
.text,
style: const TextStyle(fontSize: 16),
children: <TextSpan>[
if (widget.showTime)
TextSpan(
text: " ${_dateFormatter.format(
date: widget.message.timestamp,
showFullDate: true,
).split(' ').last}",
style: const TextStyle(
fontSize: 12,
color: Color(0xFFBBBBBB),
),
)
else
const TextSpan(),
],
),
overflow: TextOverflow.ellipsis,
maxLines: 999,
)
: CachedNetworkImage(
imageUrl:
(widget.message as ChatImageMessageModel)
.imageUrl,
),
),
],
),
),
),
],
),
);
),
],
),
);
}
}

View file

@ -23,6 +23,7 @@ class ChatDetailScreen extends StatefulWidget {
this.chatMessages,
this.onPressChatTitle,
this.iconColor,
this.showTime = false,
super.key,
});
@ -43,6 +44,7 @@ class ChatDetailScreen extends StatefulWidget {
/// The color of the icon buttons in the chat bottom.
final Color? iconColor;
final bool showTime;
@override
State<ChatDetailScreen> createState() => _ChatDetailScreenState();
@ -154,21 +156,21 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
stream: _chatMessages,
builder: (BuildContext context, snapshot) {
var messages = snapshot.data ?? widget.chat?.messages ?? [];
ChatMessageModel? lastMessage;
ChatMessageModel? previousMessage;
var messageWidgets = <Widget>[];
for (var message in messages) {
var isFirstMessage = lastMessage == null ||
lastMessage.sender.id != message.sender.id;
messageWidgets.add(
ChatDetailRow(
previousMessage: previousMessage,
showTime: widget.showTime,
translations: widget.translations,
message: message,
isFirstMessage: isFirstMessage,
userAvatarBuilder: widget.options.userAvatarBuilder,
),
);
lastMessage = message;
previousMessage = message;
}
return ListView(

View file

@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: BSD-3-Clause
// ignore_for_file: lines_longer_than_80_chars
import 'package:flutter/material.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
import 'package:flutter_community_chat_view/src/services/date_formatter.dart';
@ -16,6 +18,7 @@ class ChatScreen extends StatefulWidget {
this.deleteChatDialog,
this.unreadChats,
this.translations = const ChatTranslations(),
this.disableDismissForPermanentChats = false,
super.key,
});
@ -26,6 +29,8 @@ class ChatScreen extends StatefulWidget {
final VoidCallback? onPressStartChat;
final void Function(ChatModel chat) onDeleteChat;
final void Function(ChatModel chat) onPressChat;
/// Disable the swipe to dismiss feature for chats that are not deletable
final bool disableDismissForPermanentChats;
/// Method to optionally change the bottomsheetdialog
final Future<bool?> Function(BuildContext, ChatModel)? deleteChatDialog;
@ -76,143 +81,104 @@ class _ChatScreenState extends State<ChatScreen> {
children: [
for (ChatModel chat in snapshot.data ?? []) ...[
Builder(
builder: (context) => Dismissible(
confirmDismiss: (_) =>
widget.deleteChatDialog?.call(context, chat) ??
showModalBottomSheet(
context: context,
builder: (BuildContext context) => Container(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
chat.canBeDeleted
? translations
.deleteChatModalTitle
: translations.chatCantBeDeleted,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
if (chat.canBeDeleted)
Text(
translations
.deleteChatModalDescription,
style:
const TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
TextButton(
child: Text(
translations
.deleteChatModalCancel,
builder: (context) => !(widget.disableDismissForPermanentChats && !chat.canBeDeleted)
? Dismissible(
confirmDismiss: (_) =>
widget.deleteChatDialog
?.call(context, chat) ??
showModalBottomSheet(
context: context,
builder: (BuildContext context) =>
Container(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
chat.canBeDeleted
? translations
.deleteChatModalTitle
: translations
.chatCantBeDeleted,
style: const TextStyle(
fontSize: 16,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
onPressed: () =>
Navigator.of(context)
.pop(false),
),
if (chat.canBeDeleted)
ElevatedButton(
onPressed: () =>
Navigator.of(context)
.pop(true),
child: Text(
const SizedBox(height: 16),
if (chat.canBeDeleted)
Text(
translations
.deleteChatModalConfirm,
.deleteChatModalDescription,
style: const TextStyle(
fontSize: 16,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
TextButton(
child: Text(
translations
.deleteChatModalCancel,
style: const TextStyle(
fontSize: 16,
),
),
onPressed: () =>
Navigator.of(context)
.pop(false),
),
if (chat.canBeDeleted)
ElevatedButton(
onPressed: () =>
Navigator.of(context)
.pop(true),
child: Text(
translations
.deleteChatModalConfirm,
style: const TextStyle(
fontSize: 16,
),
),
),
],
),
],
],
),
),
],
),
onDismissed: (_) => widget.onDeleteChat(chat),
background: Container(
color: Colors.red,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
translations.deleteChatButton,
),
),
),
),
),
onDismissed: (_) => widget.onDeleteChat(chat),
background: Container(
color: Colors.red,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
translations.deleteChatButton,
key: ValueKey(
chat.id.toString(),
),
child: ChatListItem(
widget: widget,
chat: chat,
translations: translations,
dateFormatter: _dateFormatter,
),
)
: ChatListItem(
widget: widget,
chat: chat,
translations: translations,
dateFormatter: _dateFormatter,
),
),
),
key: ValueKey(
chat.id.toString(),
),
child: GestureDetector(
onTap: () => widget.onPressChat(chat),
child: widget.options.chatRowContainerBuilder(
(chat is PersonalChatModel)
? ChatRow(
unreadMessages:
chat.unreadMessages ?? 0,
avatar:
widget.options.userAvatarBuilder(
chat.user,
40.0,
),
title: chat.user.fullName ??
translations.anonymousUser,
subTitle: chat.lastMessage != null
? chat.lastMessage
is ChatTextMessageModel
? (chat.lastMessage!
as ChatTextMessageModel)
.text
: '📷 '
'${translations.image}'
: '',
lastUsed: chat.lastUsed != null
? _dateFormatter.format(
date: chat.lastUsed!,
)
: null,
)
: ChatRow(
title: (chat as GroupChatModel).title,
unreadMessages:
chat.unreadMessages ?? 0,
subTitle: chat.lastMessage != null
? chat.lastMessage
is ChatTextMessageModel
? (chat.lastMessage!
as ChatTextMessageModel)
.text
: '📷 '
'${translations.image}'
: '',
avatar:
widget.options.groupAvatarBuilder(
chat.title,
chat.imageUrl,
40.0,
),
lastUsed: chat.lastUsed != null
? _dateFormatter.format(
date: chat.lastUsed!,
)
: null,
),
),
),
),
),
],
],
@ -232,3 +198,68 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
}
class ChatListItem extends StatelessWidget {
const ChatListItem({
required this.widget,
required this.chat,
required this.translations,
required DateFormatter dateFormatter,
super.key,
}) : _dateFormatter = dateFormatter;
final ChatScreen widget;
final ChatModel chat;
final ChatTranslations translations;
final DateFormatter _dateFormatter;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => widget.onPressChat(chat),
child: widget.options.chatRowContainerBuilder(
(chat is PersonalChatModel)
? ChatRow(
unreadMessages: chat.unreadMessages ?? 0,
avatar: widget.options.userAvatarBuilder(
(chat as PersonalChatModel).user,
40.0,
),
title: (chat as PersonalChatModel).user.fullName ??
translations.anonymousUser,
subTitle: chat.lastMessage != null
? chat.lastMessage is ChatTextMessageModel
? (chat.lastMessage! as ChatTextMessageModel).text
: '📷 '
'${translations.image}'
: '',
lastUsed: chat.lastUsed != null
? _dateFormatter.format(
date: chat.lastUsed!,
)
: null,
)
: ChatRow(
title: (chat as GroupChatModel).title,
unreadMessages: chat.unreadMessages ?? 0,
subTitle: chat.lastMessage != null
? chat.lastMessage is ChatTextMessageModel
? (chat.lastMessage! as ChatTextMessageModel).text
: '📷 '
'${translations.image}'
: '',
avatar: widget.options.groupAvatarBuilder(
(chat as GroupChatModel).title,
(chat as GroupChatModel).imageUrl,
40.0,
),
lastUsed: chat.lastUsed != null
? _dateFormatter.format(
date: chat.lastUsed!,
)
: null,
),
),
);
}
}

View file

@ -4,7 +4,7 @@
name: flutter_community_chat_view
description: A standard flutter package.
version: 0.5.0
version: 0.6.0
publish_to: none
@ -20,7 +20,7 @@ dependencies:
git:
url: https://github.com/Iconica-Development/flutter_community_chat
path: packages/flutter_community_chat_interface
ref: 0.5.0
ref: 0.6.0
cached_network_image: ^3.2.2
flutter_image_picker:
git: