feat: added chat options

This commit is contained in:
mike doornenbal 2023-12-13 09:09:28 +01:00 committed by Freek van de Ven
parent 7b33ff2bd7
commit 07e29ddd99
6 changed files with 274 additions and 204 deletions

View file

@ -1,6 +1,7 @@
## 0.6.0 - December 1 2023 ## 0.6.0 - December 1 2023
- Made the message controller nullable - 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 ## 0.5.0 - November 29 2023

View file

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

View file

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

View file

@ -11,16 +11,18 @@ import 'package:flutter_community_chat_view/src/services/date_formatter.dart';
class ChatDetailRow extends StatefulWidget { class ChatDetailRow extends StatefulWidget {
const ChatDetailRow({ const ChatDetailRow({
required this.translations, required this.translations,
required this.isFirstMessage,
required this.message, required this.message,
required this.userAvatarBuilder, required this.userAvatarBuilder,
this.previousMessage,
this.showTime = false,
super.key, super.key,
}); });
final ChatTranslations translations; final ChatTranslations translations;
final bool isFirstMessage;
final ChatMessageModel message; final ChatMessageModel message;
final UserAvatarBuilder userAvatarBuilder; final UserAvatarBuilder userAvatarBuilder;
final bool showTime;
final ChatMessageModel? previousMessage;
@override @override
State<ChatDetailRow> createState() => _ChatDetailRowState(); State<ChatDetailRow> createState() => _ChatDetailRowState();
@ -30,82 +32,119 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
final DateFormatter _dateFormatter = DateFormatter(); final DateFormatter _dateFormatter = DateFormatter();
@override @override
Widget build(BuildContext context) => Padding( Widget build(BuildContext context) {
padding: EdgeInsets.only(top: widget.isFirstMessage ? 25.0 : 0), var isNewDate = widget.previousMessage != null &&
child: Row( widget.message.timestamp.day != widget.previousMessage!.timestamp.day;
crossAxisAlignment: CrossAxisAlignment.start, return Padding(
children: [ padding: EdgeInsets.only(
Opacity( top: isNewDate ||
opacity: widget.isFirstMessage ? 1 : 0, widget.previousMessage == null ||
child: Padding( widget.previousMessage?.sender.id != widget.message.sender.id
padding: const EdgeInsets.only(left: 10.0), ? 25.0
child: widget.message.sender.imageUrl != null && : 0,
widget.message.sender.imageUrl!.isNotEmpty ),
? ChatImage( child: Row(
image: widget.message.sender.imageUrl!, crossAxisAlignment: CrossAxisAlignment.start,
) children: [
: widget.userAvatarBuilder( if (isNewDate ||
widget.message.sender, widget.previousMessage == null ||
30, 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( ] else ...[
child: Container( const SizedBox(
child: Padding( width: 50,
padding: const EdgeInsets.symmetric(horizontal: 22.0), ),
child: Column( ],
crossAxisAlignment: CrossAxisAlignment.start, Expanded(
mainAxisAlignment: MainAxisAlignment.start, child: Container(
children: [ child: Padding(
if (widget.isFirstMessage) padding: const EdgeInsets.symmetric(horizontal: 22.0),
Row( child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start,
children: [ mainAxisAlignment: MainAxisAlignment.start,
Text( children: [
widget.message.sender.fullName?.toUpperCase() ?? if (isNewDate ||
widget.translations.anonymousUser, 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( style: const TextStyle(
fontSize: 14, fontSize: 12,
fontWeight: FontWeight.w500, 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.chatMessages,
this.onPressChatTitle, this.onPressChatTitle,
this.iconColor, this.iconColor,
this.showTime = false,
super.key, super.key,
}); });
@ -43,6 +44,7 @@ class ChatDetailScreen extends StatefulWidget {
/// The color of the icon buttons in the chat bottom. /// The color of the icon buttons in the chat bottom.
final Color? iconColor; final Color? iconColor;
final bool showTime;
@override @override
State<ChatDetailScreen> createState() => _ChatDetailScreenState(); State<ChatDetailScreen> createState() => _ChatDetailScreenState();
@ -154,21 +156,21 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
stream: _chatMessages, stream: _chatMessages,
builder: (BuildContext context, snapshot) { builder: (BuildContext context, snapshot) {
var messages = snapshot.data ?? widget.chat?.messages ?? []; var messages = snapshot.data ?? widget.chat?.messages ?? [];
ChatMessageModel? lastMessage; ChatMessageModel? previousMessage;
var messageWidgets = <Widget>[]; var messageWidgets = <Widget>[];
for (var message in messages) { for (var message in messages) {
var isFirstMessage = lastMessage == null ||
lastMessage.sender.id != message.sender.id;
messageWidgets.add( messageWidgets.add(
ChatDetailRow( ChatDetailRow(
previousMessage: previousMessage,
showTime: widget.showTime,
translations: widget.translations, translations: widget.translations,
message: message, message: message,
isFirstMessage: isFirstMessage,
userAvatarBuilder: widget.options.userAvatarBuilder, userAvatarBuilder: widget.options.userAvatarBuilder,
), ),
); );
lastMessage = message; previousMessage = message;
} }
return ListView( return ListView(

View file

@ -2,6 +2,8 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// ignore_for_file: lines_longer_than_80_chars
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart'; import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
import 'package:flutter_community_chat_view/src/services/date_formatter.dart'; import 'package:flutter_community_chat_view/src/services/date_formatter.dart';
@ -16,6 +18,7 @@ class ChatScreen extends StatefulWidget {
this.deleteChatDialog, this.deleteChatDialog,
this.unreadChats, this.unreadChats,
this.translations = const ChatTranslations(), this.translations = const ChatTranslations(),
this.disableDismiss = false,
super.key, super.key,
}); });
@ -26,6 +29,7 @@ class ChatScreen extends StatefulWidget {
final VoidCallback? onPressStartChat; final VoidCallback? onPressStartChat;
final void Function(ChatModel chat) onDeleteChat; final void Function(ChatModel chat) onDeleteChat;
final void Function(ChatModel chat) onPressChat; final void Function(ChatModel chat) onPressChat;
final bool disableDismiss;
/// Method to optionally change the bottomsheetdialog /// Method to optionally change the bottomsheetdialog
final Future<bool?> Function(BuildContext, ChatModel)? deleteChatDialog; final Future<bool?> Function(BuildContext, ChatModel)? deleteChatDialog;
@ -76,143 +80,104 @@ class _ChatScreenState extends State<ChatScreen> {
children: [ children: [
for (ChatModel chat in snapshot.data ?? []) ...[ for (ChatModel chat in snapshot.data ?? []) ...[
Builder( Builder(
builder: (context) => Dismissible( builder: (context) => !widget.disableDismiss
confirmDismiss: (_) => ? Dismissible(
widget.deleteChatDialog?.call(context, chat) ?? confirmDismiss: (_) =>
showModalBottomSheet( widget.deleteChatDialog
context: context, ?.call(context, chat) ??
builder: (BuildContext context) => Container( showModalBottomSheet(
padding: const EdgeInsets.all(16.0), context: context,
child: Column( builder: (BuildContext context) =>
mainAxisSize: MainAxisSize.min, Container(
children: [ padding: const EdgeInsets.all(16.0),
Text( child: Column(
chat.canBeDeleted mainAxisSize: MainAxisSize.min,
? translations children: [
.deleteChatModalTitle Text(
: translations.chatCantBeDeleted, chat.canBeDeleted
style: const TextStyle( ? translations
fontSize: 20, .deleteChatModalTitle
fontWeight: FontWeight.bold, : translations
), .chatCantBeDeleted,
),
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,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 20,
fontWeight: FontWeight.bold,
), ),
), ),
onPressed: () => const SizedBox(height: 16),
Navigator.of(context) if (chat.canBeDeleted)
.pop(false), Text(
),
if (chat.canBeDeleted)
ElevatedButton(
onPressed: () =>
Navigator.of(context)
.pop(true),
child: Text(
translations translations
.deleteChatModalConfirm, .deleteChatModalDescription,
style: const TextStyle( style: const TextStyle(
fontSize: 16, 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,
),
),
), ),
), ),
), key: ValueKey(
onDismissed: (_) => widget.onDeleteChat(chat), chat.id.toString(),
background: Container(
color: Colors.red,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
translations.deleteChatButton,
), ),
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 +197,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,
),
),
);
}
}