feat: add linter

This commit is contained in:
mike doornenbal 2024-02-02 14:50:53 +01:00
parent 797eedc835
commit 723b770ac9
35 changed files with 606 additions and 762 deletions

View file

@ -1,4 +1,9 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_iconica_analysis/analysis_options.yaml
# Additional information about this file can be found at # Possible to overwrite the rules from the package
# https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
linter:
rules:

View file

@ -16,8 +16,7 @@ dependencies:
path: ../ path: ../
flutter_chat_firebase: flutter_chat_firebase:
path: ../../flutter_chat_firebase path: ../../flutter_chat_firebase
flutter_chat_local:
path: ../../flutter_chat_local
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2022 Iconica // SPDX-FileCopyrightText: 2022 Iconica
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
///
library flutter_chat; library flutter_chat;
export 'package:flutter_chat/src/chat_entry_widget.dart'; export 'package:flutter_chat/src/chat_entry_widget.dart';

View file

@ -40,25 +40,23 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => GestureDetector(
return GestureDetector( onTap: () async =>
onTap: () => widget.onTap?.call() ??
widget.onTap?.call() ?? Navigator.of(context).pushReplacement(
Navigator.of(context).pushReplacement( MaterialPageRoute(
MaterialPageRoute( builder: (context) => chatNavigatorUserStory(
builder: (context) => chatNavigatorUserStory( context,
context, configuration: ChatUserStoryConfiguration(
configuration: ChatUserStoryConfiguration( chatService: chatService!,
chatService: chatService!, chatOptionsBuilder: (ctx) => const ChatOptions(),
chatOptionsBuilder: (ctx) => const ChatOptions(), ),
), ),
), ),
), ),
), child: StreamBuilder<int>(
child: StreamBuilder<int>( stream: chatService!.chatOverviewService.getUnreadChatsCountStream(),
stream: chatService!.chatOverviewService.getUnreadChatsCountStream(), builder: (BuildContext context, snapshot) => Stack(
builder: (BuildContext context, snapshot) {
return Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
Container( Container(
@ -96,11 +94,9 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
), ),
), ),
], ],
); ),
}, ),
), );
);
}
} }
class _AnimatedNotificationIcon extends StatefulWidget { class _AnimatedNotificationIcon extends StatefulWidget {
@ -145,7 +141,7 @@ class _AnimatedNotificationIconState extends State<_AnimatedNotificationIcon>
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.notifications != widget.notifications) { if (oldWidget.notifications != widget.notifications) {
_runAnimation(); unawaited(_runAnimation());
} }
} }
@ -155,12 +151,10 @@ class _AnimatedNotificationIconState extends State<_AnimatedNotificationIcon>
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => RotationTransition(
return RotationTransition( turns: Tween(begin: 0.0, end: -.1)
turns: Tween(begin: 0.0, end: -.1) .chain(CurveTween(curve: Curves.elasticIn))
.chain(CurveTween(curve: Curves.elasticIn)) .animate(_animationController),
.animate(_animationController), child: widget.icon,
child: widget.icon, );
);
}
} }

View file

@ -9,162 +9,176 @@ import 'package:flutter_chat_local/service/local_chat_service.dart';
Widget chatNavigatorUserStory( Widget chatNavigatorUserStory(
BuildContext context, { BuildContext context, {
ChatUserStoryConfiguration? configuration, ChatUserStoryConfiguration? configuration,
}) { }) =>
return _chatScreenRoute( _chatScreenRoute(
configuration ?? configuration ??
ChatUserStoryConfiguration( ChatUserStoryConfiguration(
chatService: LocalChatService(), chatService: LocalChatService(),
chatOptionsBuilder: (ctx) => const ChatOptions(), chatOptionsBuilder: (ctx) => const ChatOptions(),
), ),
context, context,
); );
}
Widget _chatScreenRoute( Widget _chatScreenRoute(
ChatUserStoryConfiguration configuration, BuildContext context) { ChatUserStoryConfiguration configuration,
return ChatScreen( BuildContext context,
service: configuration.chatService, ) =>
options: configuration.chatOptionsBuilder(context), ChatScreen(
onNoChats: () async => await Navigator.of(context).push( service: configuration.chatService,
MaterialPageRoute( options: configuration.chatOptionsBuilder(context),
builder: (context) => _newChatScreenRoute( onNoChats: () async => Navigator.of(context).push(
configuration,
context,
),
),
),
onPressStartChat: () async {
if (configuration.onPressStartChat != null) {
return await configuration.onPressStartChat?.call();
}
return await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => _newChatScreenRoute( builder: (context) => _newChatScreenRoute(
configuration, configuration,
context, context,
), ),
), ),
); ),
}, onPressStartChat: () async {
onPressChat: (chat) async => if (configuration.onPressStartChat != null) {
configuration.onPressChat?.call(context, chat) ?? return await configuration.onPressStartChat?.call();
await Navigator.of(context).push( }
return Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => _chatDetailScreenRoute( builder: (context) => _newChatScreenRoute(
configuration, configuration,
context, context,
chat.id!,
), ),
), ),
), );
onDeleteChat: (chat) => },
configuration.onDeleteChat?.call(context, chat) ?? onPressChat: (chat) async =>
configuration.chatService.chatOverviewService.deleteChat(chat), configuration.onPressChat?.call(context, chat) ??
deleteChatDialog: configuration.deleteChatDialog, await Navigator.of(context).push(
translations: configuration.translations, MaterialPageRoute(
); builder: (context) => _chatDetailScreenRoute(
} configuration,
context,
chat.id!,
),
),
),
onDeleteChat: (chat) async =>
configuration.onDeleteChat?.call(context, chat) ??
configuration.chatService.chatOverviewService.deleteChat(chat),
deleteChatDialog: configuration.deleteChatDialog,
translations: configuration.translations,
);
Widget _chatDetailScreenRoute(ChatUserStoryConfiguration configuration, Widget _chatDetailScreenRoute(
BuildContext context, String chatId) { ChatUserStoryConfiguration configuration,
return ChatDetailScreen( BuildContext context,
pageSize: configuration.messagePageSize, String chatId,
options: configuration.chatOptionsBuilder(context), ) =>
translations: configuration.translations, ChatDetailScreen(
service: configuration.chatService, pageSize: configuration.messagePageSize,
chatId: chatId, options: configuration.chatOptionsBuilder(context),
onMessageSubmit: (message) async { translations: configuration.translations,
configuration.onMessageSubmit?.call(message) ?? service: configuration.chatService,
configuration.chatService.chatDetailService chatId: chatId,
onMessageSubmit: (message) async {
if (configuration.onMessageSubmit != null) {
return configuration.onMessageSubmit?.call(message);
} else {
await configuration.chatService.chatDetailService
.sendTextMessage(chatId: chatId, text: message); .sendTextMessage(chatId: chatId, text: message);
configuration.afterMessageSent?.call(chatId); }
},
onUploadImage: (image) async { configuration.afterMessageSent?.call(chatId);
configuration.onUploadImage?.call(image) ?? },
configuration.chatService.chatDetailService onUploadImage: (image) async {
if (configuration.onUploadImage != null) {
return await configuration.onUploadImage?.call(image);
} else {
await configuration.chatService.chatDetailService
.sendImageMessage(chatId: chatId, image: image); .sendImageMessage(chatId: chatId, image: image);
configuration.afterMessageSent?.call(chatId); }
},
onReadChat: (chat) =>
configuration.onReadChat?.call(chat) ??
configuration.chatService.chatOverviewService.readChat(chat),
onPressChatTitle: (context, chat) async {
if (configuration.onPressChatTitle?.call(context, chat) != null) {
return configuration.onPressChatTitle?.call(context, chat);
}
return await Navigator.of(context).push( configuration.afterMessageSent?.call(chatId);
MaterialPageRoute( },
builder: (context) => _chatProfileScreenRoute( onReadChat: (chat) async =>
configuration, configuration.onReadChat?.call(chat) ??
context, configuration.chatService.chatOverviewService.readChat(chat),
chatId, onPressChatTitle: (context, chat) async {
null, if (configuration.onPressChatTitle?.call(context, chat) != null) {
return configuration.onPressChatTitle?.call(context, chat);
}
return Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => _chatProfileScreenRoute(
configuration,
context,
chatId,
null,
),
), ),
), );
); },
}, iconColor: configuration.iconColor,
iconColor: configuration.iconColor, );
);
}
Widget _chatProfileScreenRoute(ChatUserStoryConfiguration configuration, Widget _chatProfileScreenRoute(
BuildContext context, String chatId, String? userId) { ChatUserStoryConfiguration configuration,
return ChatProfileScreen( BuildContext context,
translations: configuration.translations, String chatId,
chatService: configuration.chatService, String? userId,
chatId: chatId, ) =>
userId: userId, ChatProfileScreen(
onTapUser: (user) async { translations: configuration.translations,
if (configuration.onPressUserProfile != null) { chatService: configuration.chatService,
return configuration.onPressUserProfile!.call(); chatId: chatId,
} userId: userId,
onTapUser: (user) async {
if (configuration.onPressUserProfile != null) {
return configuration.onPressUserProfile!.call();
}
return Navigator.of(context).push( return Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => _chatProfileScreenRoute( builder: (context) => _chatProfileScreenRoute(
configuration, configuration,
context, context,
chatId, chatId,
userId, userId,
),
), ),
), );
); },
}, );
);
}
Widget _newChatScreenRoute( Widget _newChatScreenRoute(
ChatUserStoryConfiguration configuration, BuildContext context) { ChatUserStoryConfiguration configuration,
return NewChatScreen( BuildContext context,
options: configuration.chatOptionsBuilder(context), ) =>
translations: configuration.translations, NewChatScreen(
service: configuration.chatService, options: configuration.chatOptionsBuilder(context),
onPressCreateChat: (user) async { translations: configuration.translations,
configuration.onPressCreateChat?.call(user); service: configuration.chatService,
if (configuration.onPressCreateChat != null) return; onPressCreateChat: (user) async {
var chat = await configuration.chatService.chatOverviewService configuration.onPressCreateChat?.call(user);
.getChatByUser(user); if (configuration.onPressCreateChat != null) return;
if (chat.id == null) { var chat = await configuration.chatService.chatOverviewService
chat = .getChatByUser(user);
await configuration.chatService.chatOverviewService.storeChatIfNot( if (chat.id == null) {
PersonalChatModel( chat = await configuration.chatService.chatOverviewService
user: user, .storeChatIfNot(
), PersonalChatModel(
); user: user,
}
if (context.mounted) {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => _chatDetailScreenRoute(
configuration,
context,
chat.id!,
), ),
), );
); }
} if (context.mounted) {
}, await Navigator.of(context).push(
); MaterialPageRoute(
} builder: (context) => _chatDetailScreenRoute(
configuration,
context,
chat.id!,
),
),
);
}
},
);

View file

@ -18,18 +18,18 @@ List<GoRoute> getChatStoryRoutes(
service: configuration.chatService, service: configuration.chatService,
options: configuration.chatOptionsBuilder(context), options: configuration.chatOptionsBuilder(context),
onNoChats: () async => onNoChats: () async =>
await context.push(ChatUserStoryRoutes.newChatScreen), context.push(ChatUserStoryRoutes.newChatScreen),
onPressStartChat: () async { onPressStartChat: () async {
if (configuration.onPressStartChat != null) { if (configuration.onPressStartChat != null) {
return await configuration.onPressStartChat?.call(); return await configuration.onPressStartChat?.call();
} }
return await context.push(ChatUserStoryRoutes.newChatScreen); return context.push(ChatUserStoryRoutes.newChatScreen);
}, },
onPressChat: (chat) => onPressChat: (chat) async =>
configuration.onPressChat?.call(context, chat) ?? configuration.onPressChat?.call(context, chat) ??
context.push(ChatUserStoryRoutes.chatDetailViewPath(chat.id!)), context.push(ChatUserStoryRoutes.chatDetailViewPath(chat.id!)),
onDeleteChat: (chat) => onDeleteChat: (chat) async =>
configuration.onDeleteChat?.call(context, chat) ?? configuration.onDeleteChat?.call(context, chat) ??
configuration.chatService.chatOverviewService.deleteChat(chat), configuration.chatService.chatOverviewService.deleteChat(chat),
deleteChatDialog: configuration.deleteChatDialog, deleteChatDialog: configuration.deleteChatDialog,
@ -59,27 +59,36 @@ List<GoRoute> getChatStoryRoutes(
service: configuration.chatService, service: configuration.chatService,
chatId: chatId!, chatId: chatId!,
onMessageSubmit: (message) async { onMessageSubmit: (message) async {
configuration.onMessageSubmit?.call(message) ?? if (configuration.onMessageSubmit != null) {
configuration.chatService.chatDetailService return configuration.onMessageSubmit?.call(message);
.sendTextMessage(chatId: chatId, text: message); } else {
await configuration.chatService.chatDetailService
.sendTextMessage(chatId: chatId, text: message);
}
configuration.afterMessageSent?.call(chatId); configuration.afterMessageSent?.call(chatId);
}, },
onUploadImage: (image) async { onUploadImage: (image) async {
configuration.onUploadImage?.call(image) ?? if (configuration.onUploadImage != null) {
configuration.chatService.chatDetailService return await configuration.onUploadImage?.call(image);
.sendImageMessage(chatId: chatId, image: image); } else {
await configuration.chatService.chatDetailService
.sendImageMessage(chatId: chatId, image: image);
}
configuration.afterMessageSent?.call(chatId); configuration.afterMessageSent?.call(chatId);
}, },
onReadChat: (chat) => onReadChat: (chat) async =>
configuration.onReadChat?.call(chat) ?? configuration.onReadChat?.call(chat) ??
configuration.chatService.chatOverviewService.readChat(chat), configuration.chatService.chatOverviewService.readChat(chat),
onPressChatTitle: (context, chat) { onPressChatTitle: (context, chat) async {
if (configuration.onPressChatTitle?.call(context, chat) != null) { if (configuration.onPressChatTitle?.call(context, chat) != null) {
return configuration.onPressChatTitle?.call(context, chat); return configuration.onPressChatTitle?.call(context, chat);
} }
return context.push( return context.push(
ChatUserStoryRoutes.chatProfileScreenPath(chat.id!, null)); ChatUserStoryRoutes.chatProfileScreenPath(chat.id!, null),
);
}, },
iconColor: configuration.iconColor, iconColor: configuration.iconColor,
); );
@ -100,27 +109,29 @@ List<GoRoute> getChatStoryRoutes(
path: ChatUserStoryRoutes.newChatScreen, path: ChatUserStoryRoutes.newChatScreen,
pageBuilder: (context, state) { pageBuilder: (context, state) {
var newChatScreen = NewChatScreen( var newChatScreen = NewChatScreen(
options: configuration.chatOptionsBuilder(context), options: configuration.chatOptionsBuilder(context),
translations: configuration.translations, translations: configuration.translations,
service: configuration.chatService, service: configuration.chatService,
onPressCreateChat: (user) async { onPressCreateChat: (user) async {
configuration.onPressCreateChat?.call(user); configuration.onPressCreateChat?.call(user);
if (configuration.onPressCreateChat != null) return; if (configuration.onPressCreateChat != null) return;
var chat = await configuration.chatService.chatOverviewService var chat = await configuration.chatService.chatOverviewService
.getChatByUser(user); .getChatByUser(user);
if (chat.id == null) { if (chat.id == null) {
chat = await configuration.chatService.chatOverviewService chat = await configuration.chatService.chatOverviewService
.storeChatIfNot( .storeChatIfNot(
PersonalChatModel( PersonalChatModel(
user: user, user: user,
), ),
); );
} }
if (context.mounted) { if (context.mounted) {
await context.push( await context.push(
ChatUserStoryRoutes.chatDetailViewPath(chat.id ?? '')); ChatUserStoryRoutes.chatDetailViewPath(chat.id ?? ''),
} );
}); }
},
);
return buildScreenWithoutTransition( return buildScreenWithoutTransition(
context: context, context: context,
state: state, state: state,
@ -150,7 +161,7 @@ List<GoRoute> getChatStoryRoutes(
return configuration.onPressUserProfile!.call(); return configuration.onPressUserProfile!.call();
} }
return await context.push( return context.push(
ChatUserStoryRoutes.chatProfileScreenPath(chatId, user), ChatUserStoryRoutes.chatProfileScreenPath(chatId, user),
); );
}, },

View file

@ -38,13 +38,15 @@ class ChatUserStoryConfiguration {
final Future<void> Function(Uint8List image)? onUploadImage; final Future<void> Function(Uint8List image)? onUploadImage;
final Future<void> Function(String text)? onMessageSubmit; final Future<void> Function(String text)? onMessageSubmit;
/// Called after a new message is sent. This can be used to do something extra like sending a push notification. /// Called after a new message is sent. This can be used to do something extra
/// like sending a push notification.
final Function(String chatId)? afterMessageSent; final Function(String chatId)? afterMessageSent;
final Future<void> Function(ChatModel chat)? onReadChat; final Future<void> Function(ChatModel chat)? onReadChat;
final Function(ChatUserModel)? onPressCreateChat; final Function(ChatUserModel)? onPressCreateChat;
final ChatOptions Function(BuildContext context) chatOptionsBuilder; final ChatOptions Function(BuildContext context) chatOptionsBuilder;
/// If true, the user will be routed to the new chat screen if there are no chats. /// If true, the user will be routed to the new chat screen if there
/// are no chats.
final bool routeToNewChatIfEmpty; final bool routeToNewChatIfEmpty;
final int messagePageSize; final int messagePageSize;

View file

@ -33,6 +33,9 @@ dependencies:
ref: 1.1.0 ref: 1.1.0
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.0 flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter: flutter:

View file

@ -1,4 +1,9 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_iconica_analysis/analysis_options.yaml
# Additional information about this file can be found at # Possible to overwrite the rules from the package
# https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
linter:
rules:

View file

@ -31,18 +31,17 @@ class FirebaseChatOptions {
String? usersCollectionName, String? usersCollectionName,
String? chatsMetaDataCollectionName, String? chatsMetaDataCollectionName,
String? userChatsCollectionName, String? userChatsCollectionName,
}) { }) =>
return FirebaseChatOptions( FirebaseChatOptions(
groupChatsCollectionName: groupChatsCollectionName:
groupChatsCollectionName ?? this.groupChatsCollectionName, groupChatsCollectionName ?? this.groupChatsCollectionName,
chatsCollectionName: chatsCollectionName ?? this.chatsCollectionName, chatsCollectionName: chatsCollectionName ?? this.chatsCollectionName,
messagesCollectionName: messagesCollectionName:
messagesCollectionName ?? this.messagesCollectionName, messagesCollectionName ?? this.messagesCollectionName,
usersCollectionName: usersCollectionName ?? this.usersCollectionName, usersCollectionName: usersCollectionName ?? this.usersCollectionName,
chatsMetaDataCollectionName: chatsMetaDataCollectionName:
chatsMetaDataCollectionName ?? this.chatsMetaDataCollectionName, chatsMetaDataCollectionName ?? this.chatsMetaDataCollectionName,
userChatsCollectionName: userChatsCollectionName:
userChatsCollectionName ?? this.userChatsCollectionName, userChatsCollectionName ?? this.userChatsCollectionName,
); );
}
} }

View file

@ -19,15 +19,6 @@ class FirebaseChatDocument {
this.lastMessage, this.lastMessage,
}); });
final String? id;
final String? title;
final String? imageUrl;
final bool personal;
final bool canBeDeleted;
final Timestamp? lastUsed;
final List<String> users;
final FirebaseMessageDocument? lastMessage;
FirebaseChatDocument.fromJson(Map<String, dynamic> json, this.id) FirebaseChatDocument.fromJson(Map<String, dynamic> json, this.id)
: title = json['title'], : title = json['title'],
imageUrl = json['image_url'], imageUrl = json['image_url'],
@ -42,6 +33,15 @@ class FirebaseChatDocument {
null, null,
); );
final String? id;
final String? title;
final String? imageUrl;
final bool personal;
final bool canBeDeleted;
final Timestamp? lastUsed;
final List<String> users;
final FirebaseMessageDocument? lastMessage;
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'title': title, 'title': title,
'image_url': imageUrl, 'image_url': imageUrl,

View file

@ -15,18 +15,18 @@ class FirebaseMessageDocument {
this.imageUrl, this.imageUrl,
}); });
final String? id;
final String sender;
final String? text;
final String? imageUrl;
final Timestamp timestamp;
FirebaseMessageDocument.fromJson(Map<String, dynamic> json, this.id) FirebaseMessageDocument.fromJson(Map<String, dynamic> json, this.id)
: sender = json['sender'], : sender = json['sender'],
text = json['text'], text = json['text'],
imageUrl = json['image_url'], imageUrl = json['image_url'],
timestamp = json['timestamp']; timestamp = json['timestamp'];
final String? id;
final String sender;
final String? text;
final String? imageUrl;
final Timestamp timestamp;
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'sender': sender, 'sender': sender,
'text': text, 'text': text,

View file

@ -13,28 +13,27 @@ class FirebaseUserDocument {
this.id, this.id,
}); });
FirebaseUserDocument.fromJson(
Map<String, Object?> json,
String id,
) : this(
id: id,
firstName:
json['first_name'] == null ? '' : json['first_name']! as String,
lastName:
json['last_name'] == null ? '' : json['last_name']! as String,
imageUrl:
json['image_url'] == null ? null : json['image_url']! as String,
);
final String? firstName; final String? firstName;
final String? lastName; final String? lastName;
final String? imageUrl; final String? imageUrl;
final String? id; final String? id;
FirebaseUserDocument.fromJson( Map<String, Object?> toJson() => {
Map<String, Object?> json, 'first_name': firstName,
String id, 'last_name': lastName,
) : this( 'image_url': imageUrl,
id: id, };
firstName:
json['first_name'] == null ? '' : json['first_name'] as String,
lastName:
json['last_name'] == null ? '' : json['last_name'] as String,
imageUrl:
json['image_url'] == null ? null : json['image_url'] as String);
Map<String, Object?> toJson() {
return {
'first_name': firstName,
'last_name': lastName,
'image_url': imageUrl,
};
}
} }

View file

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2022 Iconica // SPDX-FileCopyrightText: 2022 Iconica
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
/// A Flutter package for Firebase chat.
library flutter_chat_firebase; library flutter_chat_firebase;
export 'package:flutter_chat_firebase/service/service.dart'; export 'package:flutter_chat_firebase/service/service.dart';

View file

@ -16,6 +16,18 @@ import 'package:uuid/uuid.dart';
class FirebaseChatDetailService class FirebaseChatDetailService
with ChangeNotifier with ChangeNotifier
implements ChatDetailService { implements ChatDetailService {
FirebaseChatDetailService({
required ChatUserService userService,
FirebaseApp? app,
FirebaseChatOptions? options,
}) {
var appInstance = app ?? Firebase.app();
_db = FirebaseFirestore.instanceFor(app: appInstance);
_storage = FirebaseStorage.instanceFor(app: appInstance);
_userService = userService;
_options = options ?? const FirebaseChatOptions();
}
late final FirebaseFirestore _db; late final FirebaseFirestore _db;
late final FirebaseStorage _storage; late final FirebaseStorage _storage;
late final ChatUserService _userService; late final ChatUserService _userService;
@ -29,19 +41,6 @@ class FirebaseChatDetailService
int? chatPageSize; int? chatPageSize;
DateTime timestampToFilter = DateTime.now(); DateTime timestampToFilter = DateTime.now();
FirebaseChatDetailService({
required ChatUserService userService,
FirebaseApp? app,
FirebaseChatOptions? options,
}) {
var appInstance = app ?? Firebase.app();
_db = FirebaseFirestore.instanceFor(app: appInstance);
_storage = FirebaseStorage.instanceFor(app: appInstance);
_userService = userService;
_options = options ?? const FirebaseChatOptions();
}
Future<void> _sendMessage(String chatId, Map<String, dynamic> data) async { Future<void> _sendMessage(String chatId, Map<String, dynamic> data) async {
var currentUser = await _userService.getCurrentUser(); var currentUser = await _userService.getCurrentUser();
@ -52,7 +51,7 @@ class FirebaseChatDetailService
var message = { var message = {
'sender': currentUser.id, 'sender': currentUser.id,
'timestamp': DateTime.now(), 'timestamp': DateTime.now(),
...data ...data,
}; };
var chatReference = _db var chatReference = _db
@ -89,7 +88,8 @@ class FirebaseChatDetailService
// update the chat counter for the other users // update the chat counter for the other users
// get all users from the chat // get all users from the chat
// there is a field in the chat document called users that has a list of user ids // there is a field in the chat document called users that has a list
// of user ids
var fetchedChat = await metadataReference.get(); var fetchedChat = await metadataReference.get();
var chatUsers = fetchedChat.data()?['users'] as List<dynamic>; var chatUsers = fetchedChat.data()?['users'] as List<dynamic>;
// for all users except the message sender update the unread counter // for all users except the message sender update the unread counter
@ -112,9 +112,12 @@ class FirebaseChatDetailService
'amount_unread_messages': FieldValue.increment(1), 'amount_unread_messages': FieldValue.increment(1),
}); });
} else { } else {
await userReference.set({ await userReference.set(
'amount_unread_messages': 1, {
}, SetOptions(merge: true)); 'amount_unread_messages': 1,
},
SetOptions(merge: true),
);
} }
} }
} }
@ -124,14 +127,13 @@ class FirebaseChatDetailService
Future<void> sendTextMessage({ Future<void> sendTextMessage({
required String text, required String text,
required String chatId, required String chatId,
}) { }) =>
return _sendMessage( _sendMessage(
chatId, chatId,
{ {
'text': text, 'text': text,
}, },
); );
}
@override @override
Future<void> sendImageMessage({ Future<void> sendImageMessage({
@ -171,7 +173,9 @@ class FirebaseChatDetailService
) )
.withConverter<FirebaseMessageDocument>( .withConverter<FirebaseMessageDocument>(
fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson( fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson(
snapshot.data()!, snapshot.id), snapshot.data()!,
snapshot.id,
),
toFirestore: (user, _) => user.toJson(), toFirestore: (user, _) => user.toJson(),
) )
.snapshots(); .snapshots();
@ -181,7 +185,7 @@ class FirebaseChatDetailService
var data = message.doc.data(); var data = message.doc.data();
var sender = await _userService.getUser(data!.sender); var sender = await _userService.getUser(data!.sender);
var timestamp = DateTime.fromMillisecondsSinceEpoch( var timestamp = DateTime.fromMillisecondsSinceEpoch(
(data.timestamp).millisecondsSinceEpoch, data.timestamp.millisecondsSinceEpoch,
); );
if (timestamp.isBefore(timestampToFilter)) { if (timestamp.isBefore(timestampToFilter)) {
@ -206,16 +210,15 @@ class FirebaseChatDetailService
..._cumulativeMessages, ..._cumulativeMessages,
...messages, ...messages,
]; ];
List<ChatMessageModel> uniqueObjects = var uniqueObjects = _cumulativeMessages.toSet().toList();
_cumulativeMessages.toSet().toList();
_cumulativeMessages = uniqueObjects; _cumulativeMessages = uniqueObjects;
_cumulativeMessages _cumulativeMessages
.sort((a, b) => a.timestamp.compareTo(b.timestamp)); .sort((a, b) => a.timestamp.compareTo(b.timestamp));
notifyListeners(); notifyListeners();
}); });
}, },
onCancel: () { onCancel: () async {
_subscription?.cancel(); await _subscription?.cancel();
_subscription = null; _subscription = null;
debugPrint('Canceling messages stream'); debugPrint('Canceling messages stream');
}, },
@ -225,10 +228,10 @@ class FirebaseChatDetailService
} }
@override @override
void stopListeningForMessages() { Future<void> stopListeningForMessages() async {
_subscription?.cancel(); await _subscription?.cancel();
_subscription = null; _subscription = null;
_controller?.close(); await _controller?.close();
_controller = null; _controller = null;
} }
@ -241,8 +244,9 @@ class FirebaseChatDetailService
lastChat = chatId; lastChat = chatId;
lastMessage = null; lastMessage = null;
} }
// get the x amount of last messages from the oldest message that is in cumulative messages and add that to the list // get the x amount of last messages from the oldest message that is in
List<ChatMessageModel> messages = []; //cumulative messages and add that to the list
var messages = <ChatMessageModel>[];
QuerySnapshot<FirebaseMessageDocument>? messagesQuerySnapshot; QuerySnapshot<FirebaseMessageDocument>? messagesQuerySnapshot;
var query = _db var query = _db
.collection(_options.chatsCollectionName) .collection(_options.chatsCollectionName)
@ -275,14 +279,14 @@ class FirebaseChatDetailService
} }
} }
List<FirebaseMessageDocument> messageDocuments = messagesQuerySnapshot.docs var messageDocuments = messagesQuerySnapshot.docs
.map((QueryDocumentSnapshot<FirebaseMessageDocument> doc) => doc.data()) .map((QueryDocumentSnapshot<FirebaseMessageDocument> doc) => doc.data())
.toList(); .toList();
for (var message in messageDocuments) { for (var message in messageDocuments) {
var sender = await _userService.getUser(message.sender); var sender = await _userService.getUser(message.sender);
if (sender != null) { if (sender != null) {
var timestamp = DateTime.fromMillisecondsSinceEpoch( var timestamp = DateTime.fromMillisecondsSinceEpoch(
(message.timestamp).millisecondsSinceEpoch, message.timestamp.millisecondsSinceEpoch,
); );
messages.add( messages.add(
@ -310,7 +314,5 @@ class FirebaseChatDetailService
} }
@override @override
List<ChatMessageModel> getMessages() { List<ChatMessageModel> getMessages() => _cumulativeMessages;
return _cumulativeMessages;
}
} }

View file

@ -1,4 +1,5 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first // ignore: lines_longer_than_80_chars
// ignore_for_file: public_member_api_docs, sort_constructors_first, close_sinks, avoid_dynamic_calls
// SPDX-FileCopyrightText: 2022 Iconica // SPDX-FileCopyrightText: 2022 Iconica
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
@ -49,6 +50,7 @@ class FirebaseChatOverviewService implements ChatOverviewService {
Stream<List<ChatModel>> getChatsStream() { Stream<List<ChatModel>> getChatsStream() {
StreamSubscription? chatSubscription; StreamSubscription? chatSubscription;
late StreamController<List<ChatModel>> controller; late StreamController<List<ChatModel>> controller;
controller = StreamController( controller = StreamController(
onListen: () async { onListen: () async {
var currentUser = await _userService.getCurrentUser(); var currentUser = await _userService.getCurrentUser();
@ -68,11 +70,13 @@ class FirebaseChatOverviewService implements ChatOverviewService {
) )
.withConverter( .withConverter(
fromFirestore: (snapshot, _) => FirebaseChatDocument.fromJson( fromFirestore: (snapshot, _) => FirebaseChatDocument.fromJson(
snapshot.data()!, snapshot.id), snapshot.data()!,
snapshot.id,
),
toFirestore: (chat, _) => chat.toJson(), toFirestore: (chat, _) => chat.toJson(),
) )
.snapshots(); .snapshots();
List<ChatModel> chats = []; var chats = <ChatModel>[];
ChatModel? chatModel; ChatModel? chatModel;
chatSubscription = chatSnapshot.listen((event) async { chatSubscription = chatSnapshot.listen((event) async {
@ -84,7 +88,7 @@ class FirebaseChatOverviewService implements ChatOverviewService {
(element) => element != currentUser?.id, (element) => element != currentUser?.id,
), ),
); );
int? unread = var unread =
await _addUnreadChatSubscription(chat.id!, currentUser!.id!); await _addUnreadChatSubscription(chat.id!, currentUser!.id!);
if (chat.personal) { if (chat.personal) {
@ -157,10 +161,10 @@ class FirebaseChatOverviewService implements ChatOverviewService {
} }
chats.add(chatModel!); chats.add(chatModel!);
} }
Set<String> uniqueIds = <String>{}; var uniqueIds = <String>{};
List<ChatModel> uniqueChatModels = []; var uniqueChatModels = <ChatModel>[];
for (ChatModel chatModel in chats) { for (var chatModel in chats) {
if (uniqueIds.add(chatModel.id!)) { if (uniqueIds.add(chatModel.id!)) {
uniqueChatModels.add(chatModel); uniqueChatModels.add(chatModel);
} else { } else {
@ -189,8 +193,8 @@ class FirebaseChatOverviewService implements ChatOverviewService {
}); });
}); });
}, },
onCancel: () { onCancel: () async {
chatSubscription?.cancel(); await chatSubscription?.cancel();
}, },
); );
return controller.stream; return controller.stream;
@ -278,7 +282,7 @@ class FirebaseChatOverviewService implements ChatOverviewService {
if (chatData != null) { if (chatData != null) {
for (var userId in chatData.users) { for (var userId in chatData.users) {
_db await _db
.collection(_options.usersCollectionName) .collection(_options.usersCollectionName)
.doc(userId) .doc(userId)
.collection(_options.userChatsCollectionName) .collection(_options.userChatsCollectionName)
@ -349,7 +353,7 @@ class FirebaseChatOverviewService implements ChatOverviewService {
return chat; return chat;
} }
List<String> userIds = [ var userIds = <String>[
currentUser!.id!, currentUser!.id!,
...chat.users.map((e) => e.id!), ...chat.users.map((e) => e.id!),
]; ];
@ -390,9 +394,11 @@ class FirebaseChatOverviewService implements ChatOverviewService {
@override @override
Stream<int> getUnreadChatsCountStream() { Stream<int> getUnreadChatsCountStream() {
// open a stream to the user's chats collection and listen to changes in this collection we will also add the amount of read chats // open a stream to the user's chats collection and listen to changes in
// this collection we will also add the amount of read chats
StreamSubscription? unreadChatSubscription; StreamSubscription? unreadChatSubscription;
late StreamController<int> controller; late StreamController<int> controller;
controller = StreamController( controller = StreamController(
onListen: () async { onListen: () async {
var currentUser = await _userService.getCurrentUser(); var currentUser = await _userService.getCurrentUser();
@ -403,17 +409,20 @@ class FirebaseChatOverviewService implements ChatOverviewService {
.snapshots(); .snapshots();
unreadChatSubscription = userSnapshot.listen((event) { unreadChatSubscription = userSnapshot.listen((event) {
// every chat has a field called amount_unread_messages, combine all of these fields to get the total amount of unread messages // every chat has a field called amount_unread_messages, combine all
// of these fields to get the total amount of unread messages
var unreadChats = event.docs var unreadChats = event.docs
.map((chat) => chat.data()['amount_unread_messages'] ?? 0) .map((chat) => chat.data()['amount_unread_messages'] ?? 0)
.toList(); .toList();
var totalUnreadChats = unreadChats.fold<int>( var totalUnreadChats = unreadChats.fold<int>(
0, (previousValue, element) => previousValue + (element as int)); 0,
(previousValue, element) => previousValue + (element as int),
);
controller.add(totalUnreadChats); controller.add(totalUnreadChats);
}); });
}, },
onCancel: () { onCancel: () async {
unreadChatSubscription?.cancel(); await unreadChatSubscription?.cancel();
}, },
); );
return controller.stream; return controller.stream;
@ -429,7 +438,7 @@ class FirebaseChatOverviewService implements ChatOverviewService {
// set the amount of unread messages to 0 // set the amount of unread messages to 0
await _db await _db
.collection(_options.usersCollectionName) .collection(_options.usersCollectionName)
.doc(currentUser!.id!) .doc(currentUser!.id)
.collection(_options.userChatsCollectionName) .collection(_options.userChatsCollectionName)
.doc(chat.id) .doc(chat.id)
.set({'amount_unread_messages': 0}, SetOptions(merge: true)); .set({'amount_unread_messages': 0}, SetOptions(merge: true));

View file

@ -1,4 +1,4 @@
export 'package:flutter_chat_firebase/service/firebase_chat_user_service.dart';
export 'package:flutter_chat_firebase/service/firebase_chat_detail_service.dart'; export 'package:flutter_chat_firebase/service/firebase_chat_detail_service.dart';
export 'package:flutter_chat_firebase/service/firebase_chat_overview_service.dart'; export 'package:flutter_chat_firebase/service/firebase_chat_overview_service.dart';
export 'package:flutter_chat_firebase/service/firebase_chat_service.dart'; export 'package:flutter_chat_firebase/service/firebase_chat_service.dart';
export 'package:flutter_chat_firebase/service/firebase_chat_user_service.dart';

View file

@ -26,6 +26,9 @@ dependencies:
ref: 1.1.0 ref: 1.1.0
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.0 flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter: flutter:

View file

@ -1,4 +1,9 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_iconica_analysis/analysis_options.yaml
# Additional information about this file can be found at # Possible to overwrite the rules from the package
# https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
linter:
rules:

View file

@ -1,9 +1,9 @@
// SPDX-FileCopyrightText: 2022 Iconica // SPDX-FileCopyrightText: 2022 Iconica
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
/// A Flutter package for Firebase chat.
library flutter_chat_interface; library flutter_chat_interface;
export 'package:flutter_chat_interface/src/chat_data_provider.dart';
export 'package:flutter_chat_interface/src/model/model.dart'; export 'package:flutter_chat_interface/src/model/model.dart';
export 'package:flutter_chat_interface/src/service/service.dart'; export 'package:flutter_chat_interface/src/service/service.dart';
export 'package:flutter_chat_interface/src/chat_data_provider.dart';

View file

@ -34,15 +34,15 @@ abstract class GroupChatModelInterface extends ChatModel {
class GroupChatModel implements GroupChatModelInterface { class GroupChatModel implements GroupChatModelInterface {
GroupChatModel({ GroupChatModel({
required this.canBeDeleted,
required this.title,
required this.imageUrl,
required this.users,
this.id, this.id,
this.messages, this.messages,
this.unreadMessages, this.unreadMessages,
this.lastUsed, this.lastUsed,
this.lastMessage, this.lastMessage,
required this.canBeDeleted,
required this.title,
required this.imageUrl,
required this.users,
}); });
@override @override
@ -75,17 +75,16 @@ class GroupChatModel implements GroupChatModelInterface {
String? title, String? title,
String? imageUrl, String? imageUrl,
List<ChatUserModel>? users, List<ChatUserModel>? users,
}) { }) =>
return GroupChatModel( GroupChatModel(
id: id ?? this.id, id: id ?? this.id,
messages: messages ?? this.messages, messages: messages ?? this.messages,
unreadMessages: unreadMessages ?? this.unreadMessages, unreadMessages: unreadMessages ?? this.unreadMessages,
lastUsed: lastUsed ?? this.lastUsed, lastUsed: lastUsed ?? this.lastUsed,
lastMessage: lastMessage ?? this.lastMessage, lastMessage: lastMessage ?? this.lastMessage,
canBeDeleted: canBeDeleted ?? this.canBeDeleted, canBeDeleted: canBeDeleted ?? this.canBeDeleted,
title: title ?? this.title, title: title ?? this.title,
imageUrl: imageUrl ?? this.imageUrl, imageUrl: imageUrl ?? this.imageUrl,
users: users ?? this.users, users: users ?? this.users,
); );
}
} }

View file

@ -1,7 +1,7 @@
export 'chat.dart'; export 'chat.dart';
export 'chat_image_message.dart'; export 'chat_image_message.dart';
export 'chat_message.dart';
export 'chat_text_message.dart'; export 'chat_text_message.dart';
export 'chat_user.dart'; export 'chat_user.dart';
export 'group_chat.dart'; export 'group_chat.dart';
export 'personal_chat.dart'; export 'personal_chat.dart';
export 'chat_message.dart';

View file

@ -30,13 +30,13 @@ abstract class PersonalChatModelInterface extends ChatModel {
class PersonalChatModel implements PersonalChatModelInterface { class PersonalChatModel implements PersonalChatModelInterface {
PersonalChatModel({ PersonalChatModel({
required this.user,
this.id, this.id,
this.messages = const [], this.messages = const [],
this.unreadMessages, this.unreadMessages,
this.lastUsed, this.lastUsed,
this.lastMessage, this.lastMessage,
this.canBeDeleted = true, this.canBeDeleted = true,
required this.user,
}); });
@override @override
@ -64,15 +64,14 @@ class PersonalChatModel implements PersonalChatModelInterface {
ChatMessageModel? lastMessage, ChatMessageModel? lastMessage,
bool? canBeDeleted, bool? canBeDeleted,
ChatUserModel? user, ChatUserModel? user,
}) { }) =>
return PersonalChatModel( PersonalChatModel(
id: id ?? this.id, id: id ?? this.id,
messages: messages ?? this.messages, messages: messages ?? this.messages,
unreadMessages: unreadMessages ?? this.unreadMessages, unreadMessages: unreadMessages ?? this.unreadMessages,
lastUsed: lastUsed ?? this.lastUsed, lastUsed: lastUsed ?? this.lastUsed,
lastMessage: lastMessage ?? this.lastMessage, lastMessage: lastMessage ?? this.lastMessage,
user: user ?? this.user, user: user ?? this.user,
canBeDeleted: canBeDeleted ?? this.canBeDeleted, canBeDeleted: canBeDeleted ?? this.canBeDeleted,
); );
}
} }

View file

@ -1,4 +1,4 @@
export 'chat_overview_service.dart';
export 'user_service.dart';
export 'chat_detail_service.dart'; export 'chat_detail_service.dart';
export 'chat_overview_service.dart';
export 'chat_service.dart'; export 'chat_service.dart';
export 'user_service.dart';

View file

@ -20,6 +20,9 @@ dependencies:
ref: 1.0.0 ref: 1.0.0
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.0 flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter: flutter:

View file

@ -20,5 +20,8 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter: flutter:

View file

@ -1,214 +1,9 @@
# SPDX-FileCopyrightText: 2022 Iconica include: package:flutter_iconica_analysis/analysis_options.yaml
#
# SPDX-License-Identifier: GPL-3.0-or-later # Possible to overwrite the rules from the package
include: package:flutter_lints/flutter.yaml
analyzer: analyzer:
errors: exclude:
todo: ignore
exclude: [lib/generated_plugin_registrant.dart]
linter:
# https://dart.dev/tools/linter-rules#lints
rules:
# error rules
always_use_package_imports: false
avoid_dynamic_calls: true
avoid_empty_else: true
avoid_print: true
avoid_relative_lib_imports: true
avoid_returning_null_for_future: true
avoid_slow_async_io: true
avoid_type_to_string: true
avoid_types_as_parameter_names: true
avoid_web_libraries_in_flutter: true
cancel_subscriptions: true
close_sinks: true
comment_references: false
control_flow_in_finally: true
diagnostic_describe_all_properties: false
empty_statements: true
hash_and_equals: true
literal_only_boolean_expressions: true
no_adjacent_strings_in_list: true
no_duplicate_case_values: true
no_logic_in_create_state: true
prefer_relative_imports: false
prefer_void_to_null: true
test_types_in_equals: true
throw_in_finally: true
unnecessary_statements: true
unrelated_type_equality_checks: true
unsafe_html: true
use_build_context_synchronously: true
use_key_in_widget_constructors: true
valid_regexps: true
# style rules
always_declare_return_types: true
always_put_control_body_on_new_line: true
always_put_required_named_parameters_first: true
always_require_non_null_named_parameters: true
always_specify_types: false
annotate_overrides: true
avoid_annotating_with_dynamic: false
avoid_bool_literals_in_conditional_expressions: true
avoid_catches_without_on_clauses: false
avoid_catching_errors: false
avoid_classes_with_only_static_members: true
avoid_double_and_int_checks: true
avoid_equals_and_hash_code_on_mutable_classes: false
avoid_escaping_inner_quotes: false
avoid_field_initializers_in_const_classes: true
avoid_final_parameters: true
avoid_function_literals_in_foreach_calls: true
avoid_implementing_value_types: true
avoid_init_to_null: true
avoid_js_rounded_ints: true
avoid_multiple_declarations_per_line: true
avoid_null_checks_in_equality_operators: true
avoid_positional_boolean_parameters: true
avoid_private_typedef_functions: true
avoid_redundant_argument_values: false
avoid_renaming_method_parameters: true
avoid_return_types_on_setters: true
avoid_returning_null: true
avoid_returning_null_for_void: true
avoid_returning_this: true
avoid_setters_without_getters: true
avoid_shadowing_type_parameters: true
avoid_single_cascade_in_expression_statements: true
avoid_types_on_closure_parameters: false
avoid_unnecessary_containers: false
avoid_unused_constructor_parameters: true
avoid_void_async: true
await_only_futures: true
camel_case_extensions: true
camel_case_types: true
cascade_invocations: true
cast_nullable_to_non_nullable: true
conditional_uri_does_not_exist: true
constant_identifier_names: true
curly_braces_in_flow_control_structures: true
deprecated_consistency: true
directives_ordering: true
do_not_use_environment: true
empty_catches: true
empty_constructor_bodies: true
eol_at_end_of_file: true
exhaustive_cases: true
file_names: true
flutter_style_todos: true
implementation_imports: true
join_return_with_assignment: true
leading_newlines_in_multiline_strings: true
library_names: true
library_prefixes: true
library_private_types_in_public_api: true
lines_longer_than_80_chars: true
missing_whitespace_between_adjacent_strings: true
no_default_cases: true
no_leading_underscores_for_library_prefixes: true
no_leading_underscores_for_local_identifiers: true
no_runtimeType_toString: true
non_constant_identifier_names: true
noop_primitive_operations: true
null_check_on_nullable_type_parameter: true
null_closures: true
omit_local_variable_types: true
one_member_abstracts: true
only_throw_errors: true
overridden_fields: true
package_api_docs: true
package_prefixed_library_names: true
parameter_assignments: true
prefer_adjacent_string_concatenation: true
prefer_asserts_in_initializer_lists: true
prefer_asserts_with_message: true
prefer_collection_literals: true
prefer_conditional_assignment: true
prefer_const_constructors: true
prefer_const_constructors_in_immutables: true
prefer_const_declarations: false
prefer_const_literals_to_create_immutables: false
prefer_constructors_over_static_methods: true
prefer_contains: true
prefer_double_quotes: false
prefer_equal_for_default_values: true
prefer_expression_function_bodies: false
prefer_final_fields: true
prefer_final_in_for_each: false
prefer_final_locals: false
prefer_final_parameters: false
prefer_for_elements_to_map_fromIterable: true
prefer_foreach: true
prefer_function_declarations_over_variables: true
prefer_generic_function_type_aliases: true
prefer_if_elements_to_conditional_expressions: true
prefer_if_null_operators: true
prefer_initializing_formals: true
prefer_inlined_adds: true
prefer_int_literals: false
prefer_interpolation_to_compose_strings: true
prefer_is_empty: true
prefer_is_not_empty: true
prefer_is_not_operator: true
prefer_iterable_whereType: true
prefer_mixin: true
prefer_null_aware_method_calls: true
prefer_null_aware_operators: true
prefer_single_quotes: true
prefer_spread_collections: true
prefer_typing_uninitialized_variables: true
provide_deprecation_message: true
public_member_api_docs: false
recursive_getters: true
require_trailing_commas: true
sized_box_for_whitespace: true
sized_box_shrink_expand: true
slash_for_doc_comments: true
sort_child_properties_last: true
sort_constructors_first: true
sort_unnamed_constructors_first: true
tighten_type_of_initializing_formals: true
type_annotate_public_apis: true
type_init_formals: true
unawaited_futures: true
unnecessary_await_in_return: true
unnecessary_brace_in_string_interps: true
unnecessary_const: false
unnecessary_constructor_name: true
unnecessary_final: true
unnecessary_getters_setters: true
unnecessary_lambdas: true
unnecessary_late: true
unnecessary_new: true
unnecessary_null_aware_assignments: true
unnecessary_null_checks: true
unnecessary_null_in_if_null_operators: true
unnecessary_nullable_for_final_variable_declarations: true
unnecessary_overrides: true
unnecessary_parenthesis: true
unnecessary_raw_strings: true
unnecessary_string_escapes: true
unnecessary_string_interpolations: true
unnecessary_this: true
use_decorated_box: true
use_full_hex_values_for_flutter_colors: true
use_function_type_syntax_for_parameters: true
use_if_null_to_convert_nulls_to_bools: true
use_is_even_rather_than_modulo: true
use_late_for_private_fields_and_variables: true
use_named_constants: true
use_raw_strings: false
use_rethrow_when_possible: true
use_setters_to_change_properties: true
use_string_buffers: true
use_test_throws_matchers: true
use_to_and_as_if_applicable: true
void_checks: true
# pub rules
depend_on_referenced_packages: true
secure_pubspec_urls: false
sort_pub_dependencies: false
# Additional information about this file can be found at linter:
# https://dart.dev/guides/language/analysis-options rules:

View file

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2022 Iconica // SPDX-FileCopyrightText: 2022 Iconica
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
/// A Flutter package for Firebase chat.
library flutter_chat_view; library flutter_chat_view;
export 'package:flutter_chat_interface/flutter_chat_interface.dart'; export 'package:flutter_chat_interface/flutter_chat_interface.dart';

View file

@ -53,11 +53,11 @@ class _ChatBottomState extends State<ChatBottom> {
), ),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () async {
var value = _textEditingController.text; var value = _textEditingController.text;
if (value.isNotEmpty) { if (value.isNotEmpty) {
widget.onMessageSubmit(value); await widget.onMessageSubmit(value);
_textEditingController.clear(); _textEditingController.clear();
} }
}, },

View file

@ -64,85 +64,80 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
), ),
], ],
Expanded( Expanded(
child: Container( child: Padding(
child: Padding( padding: const EdgeInsets.symmetric(horizontal: 22.0),
padding: const EdgeInsets.symmetric(horizontal: 22.0), child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, children: [
children: [ if (isNewDate || isSameSender)
if (isNewDate || isSameSender) Row(
Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ Text(
Text( widget.message.sender.fullName?.toUpperCase() ??
widget.message.sender.fullName?.toUpperCase() ?? widget.translations.anonymousUser,
widget.translations.anonymousUser, style: TextStyle(
style: TextStyle( fontSize: 14,
fontSize: 14, fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500, color:
color: Theme.of(context) Theme.of(context).textTheme.labelMedium?.color,
.textTheme ),
.labelMedium ),
?.color, 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: 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
? RichText(
text: TextSpan(
text: (widget.message as ChatTextMessageModel)
.text,
style: TextStyle(
fontSize: 16,
color: Theme.of(context)
.textTheme
.labelMedium
?.color,
),
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,
),
), ),
], Padding(
), padding: const EdgeInsets.only(top: 3.0),
child: widget.message is ChatTextMessageModel
? RichText(
text: TextSpan(
text:
(widget.message as ChatTextMessageModel).text,
style: TextStyle(
fontSize: 16,
color: Theme.of(context)
.textTheme
.labelMedium
?.color,
),
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

@ -2,6 +2,8 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// ignore_for_file: discarded_futures
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
@ -123,7 +125,7 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Future<void> onPressSelectImage() => showModalBottomSheet<Uint8List?>( Future<void> onPressSelectImage() async => showModalBottomSheet<Uint8List?>(
context: context, context: context,
builder: (BuildContext context) => builder: (BuildContext context) =>
widget.options.imagePickerContainerBuilder( widget.options.imagePickerContainerBuilder(
@ -199,7 +201,7 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
var isTop = controller.position.pixels == var isTop = controller.position.pixels ==
controller.position.maxScrollExtent; controller.position.maxScrollExtent;
if (showIndicator == false && if (!showIndicator &&
!isTop && !isTop &&
controller.position.userScrollDirection == controller.position.userScrollDirection ==
ScrollDirection.reverse) { ScrollDirection.reverse) {

View file

@ -1,3 +1,5 @@
// ignore_for_file: discarded_futures
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_chat_view/flutter_chat_view.dart'; import 'package:flutter_chat_view/flutter_chat_view.dart';
import 'package:flutter_chat_view/src/services/profile_service.dart'; import 'package:flutter_chat_view/src/services/profile_service.dart';

View file

@ -258,55 +258,53 @@ class ChatListItem extends StatelessWidget {
final DateFormatter _dateFormatter; final DateFormatter _dateFormatter;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => GestureDetector(
return GestureDetector( onTap: () => widget.onPressChat(chat),
onTap: () => widget.onPressChat(chat), child: Container(
child: Container( color: Colors.transparent,
color: Colors.transparent, child: widget.options.chatRowContainerBuilder(
child: widget.options.chatRowContainerBuilder( (chat is PersonalChatModel)
(chat is PersonalChatModel) ? ChatRow(
? ChatRow( unreadMessages: chat.unreadMessages ?? 0,
unreadMessages: chat.unreadMessages ?? 0, avatar: widget.options.userAvatarBuilder(
avatar: widget.options.userAvatarBuilder( (chat as PersonalChatModel).user,
(chat as PersonalChatModel).user, 40.0,
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,
), ),
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

@ -29,67 +29,62 @@ class _NewChatScreenState extends State<NewChatScreen> {
String query = ''; String query = '';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => Scaffold(
return Scaffold( appBar: AppBar(
appBar: AppBar( title: _buildSearchField(),
title: _buildSearchField(), actions: [
actions: [ _buildSearchIcon(),
_buildSearchIcon(), ],
], ),
), body: FutureBuilder<List<ChatUserModel>>(
body: FutureBuilder<List<ChatUserModel>>( // ignore: discarded_futures
future: widget.service.chatUserService.getAllUsers(), future: widget.service.chatUserService.getAllUsers(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}'); return Text('Error: ${snapshot.error}');
} else if (snapshot.hasData) { } else if (snapshot.hasData) {
return _buildUserList(snapshot.data!); return _buildUserList(snapshot.data!);
} else { } else {
return widget.options return widget.options
.noChatsPlaceholderBuilder(widget.translations); .noChatsPlaceholderBuilder(widget.translations);
}
},
),
);
Widget _buildSearchField() => _isSearching
? Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextField(
focusNode: _textFieldFocusNode,
onChanged: (value) {
setState(() {
query = value;
});
},
decoration: InputDecoration(
hintText: widget.translations.searchPlaceholder,
),
),
)
: Text(widget.translations.newChatButton);
Widget _buildSearchIcon() => IconButton(
onPressed: () {
setState(() {
_isSearching = !_isSearching;
});
if (_isSearching) {
_textFieldFocusNode.requestFocus();
} }
}, },
), icon: Icon(
); _isSearching ? Icons.close : Icons.search,
} ),
);
Widget _buildSearchField() {
return _isSearching
? Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextField(
focusNode: _textFieldFocusNode,
onChanged: (value) {
setState(() {
query = value;
});
},
decoration: InputDecoration(
hintText: widget.translations.searchPlaceholder,
),
),
)
: Text(widget.translations.newChatButton);
}
Widget _buildSearchIcon() {
return IconButton(
onPressed: () {
setState(() {
_isSearching = !_isSearching;
});
if (_isSearching) {
_textFieldFocusNode.requestFocus();
}
},
icon: Icon(
_isSearching ? Icons.close : Icons.search,
),
);
}
Widget _buildUserList(List<ChatUserModel> users) { Widget _buildUserList(List<ChatUserModel> users) {
var filteredUsers = users var filteredUsers = users

View file

@ -34,6 +34,9 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 6.0.0
flutter: flutter: