feat: ui update

This commit is contained in:
mike doornenbal 2024-07-17 13:01:21 +02:00 committed by Freek van de Ven
parent 644615f026
commit 8f13d87a23
28 changed files with 1195 additions and 611 deletions

View file

@ -1,5 +1,8 @@
## 3.1.0 ## 3.1.0
- Fix center the texts for no users found with search and type first message - Fix center the texts for no users found with search and type first message
- Fix styling for the whole userstory
- Add groupchat profile picture, and bio to the groupchat creation screen
- Updated profile of users and groups
## 3.0.1 ## 3.0.1

View file

@ -16,15 +16,6 @@ dependencies:
path: ../ path: ../
flutter_chat_firebase: flutter_chat_firebase:
path: ../../flutter_chat_firebase path: ../../flutter_chat_firebase
dependency_overrides:
flutter_chat:
path: ../../flutter_chat
flutter_chat_interface:
path: ../../flutter_chat_interface
flutter_chat_local:
path: ../../flutter_chat_local
flutter_chat_view:
path: ../../flutter_chat_view
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -104,6 +104,10 @@ Widget _chatDetailScreenRoute(
if (configuration.onPressUserProfile != null) { if (configuration.onPressUserProfile != null) {
return configuration.onPressUserProfile?.call(context, user); return configuration.onPressUserProfile?.call(context, user);
} }
var currentUser =
await configuration.chatService.chatUserService.getCurrentUser();
var currentUserId = currentUser!.id!;
if (context.mounted)
return Navigator.of(context).push( return Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => _chatProfileScreenRoute( builder: (context) => _chatProfileScreenRoute(
@ -111,6 +115,7 @@ Widget _chatDetailScreenRoute(
context, context,
chatId, chatId,
user.id, user.id,
currentUserId,
), ),
), ),
); );
@ -142,7 +147,10 @@ Widget _chatDetailScreenRoute(
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);
} }
var currentUser =
await configuration.chatService.chatUserService.getCurrentUser();
var currentUserId = currentUser!.id!;
if (context.mounted)
return Navigator.of(context).push( return Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => _chatProfileScreenRoute( builder: (context) => _chatProfileScreenRoute(
@ -150,6 +158,7 @@ Widget _chatDetailScreenRoute(
context, context,
chatId, chatId,
null, null,
currentUserId,
), ),
), ),
); );
@ -168,17 +177,23 @@ Widget _chatProfileScreenRoute(
BuildContext context, BuildContext context,
String chatId, String chatId,
String? userId, String? userId,
String currentUserId,
) => ) =>
ChatProfileScreen( ChatProfileScreen(
options: configuration.chatOptionsBuilder(context),
translations: configuration.translations, translations: configuration.translations,
chatService: configuration.chatService, chatService: configuration.chatService,
chatId: chatId, chatId: chatId,
userId: userId, userId: userId,
currentUserId: currentUserId,
onTapUser: (user) async { onTapUser: (user) async {
if (configuration.onPressUserProfile != null) { if (configuration.onPressUserProfile != null) {
return configuration.onPressUserProfile!.call(context, user); return configuration.onPressUserProfile!.call(context, user);
} }
var currentUser =
await configuration.chatService.chatUserService.getCurrentUser();
var currentUserId = currentUser!.id!;
if (context.mounted)
return Navigator.of(context).push( return Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => _chatProfileScreenRoute( builder: (context) => _chatProfileScreenRoute(
@ -186,10 +201,40 @@ Widget _chatProfileScreenRoute(
context, context,
chatId, chatId,
user.id, user.id,
currentUserId,
), ),
), ),
); );
}, },
onPressStartChat: (user) async {
configuration.onPressCreateChat?.call(user);
if (configuration.onPressCreateChat != null) return;
var chat = await configuration.chatService.chatOverviewService
.getChatByUser(user);
if (chat.id == null) {
chat = await configuration.chatService.chatOverviewService
.storeChatIfNot(
PersonalChatModel(
user: user,
),
null,
);
}
if (context.mounted) {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PopScope(
canPop: false,
child: _chatDetailScreenRoute(
configuration,
context,
chat.id!,
),
),
),
);
}
},
); );
/// Constructs the new chat screen route widget. /// Constructs the new chat screen route widget.
@ -207,6 +252,8 @@ Widget _newChatScreenRoute(
showGroupChatButton: configuration.enableGroupChatCreation, showGroupChatButton: configuration.enableGroupChatCreation,
onPressCreateGroupChat: () async { onPressCreateGroupChat: () async {
configuration.onPressCreateGroupChat?.call(); configuration.onPressCreateGroupChat?.call();
configuration.chatService.chatOverviewService
.clearCurrentlySelectedUsers();
if (context.mounted) { if (context.mounted) {
await Navigator.of(context).push( await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
@ -223,13 +270,13 @@ Widget _newChatScreenRoute(
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);
debugPrint("Chat is ${chat.id}");
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,
), ),
null,
); );
} }
if (context.mounted) { if (context.mounted) {
@ -277,19 +324,20 @@ Widget _newGroupChatOverviewScreenRoute(
options: configuration.chatOptionsBuilder(context), options: configuration.chatOptionsBuilder(context),
translations: configuration.translations, translations: configuration.translations,
service: configuration.chatService, service: configuration.chatService,
users: users, onPressCompleteGroupChatCreation:
onPressCompleteGroupChatCreation: (users, groupChatName) async { (users, groupChatName, groupBio, image) async {
configuration.onPressCompleteGroupChatCreation configuration.onPressCompleteGroupChatCreation
?.call(users, groupChatName); ?.call(users, groupChatName, image);
if (configuration.onPressCreateGroupChat != null) return; if (configuration.onPressCreateGroupChat != null) return;
var chat = var chat =
await configuration.chatService.chatOverviewService.storeChatIfNot( await configuration.chatService.chatOverviewService.storeChatIfNot(
GroupChatModel( GroupChatModel(
canBeDeleted: true, canBeDeleted: true,
title: groupChatName, title: groupChatName,
imageUrl: "https://picsum.photos/200/300",
users: users, users: users,
bio: groupBio,
), ),
image,
); );
if (context.mounted) { if (context.mounted) {
await Navigator.of(context).pushReplacement( await Navigator.of(context).pushReplacement(

View file

@ -152,6 +152,7 @@ List<GoRoute> getChatStoryRoutes(
PersonalChatModel( PersonalChatModel(
user: user, user: user,
), ),
null,
); );
} }
if (context.mounted) { if (context.mounted) {
@ -160,9 +161,13 @@ List<GoRoute> getChatStoryRoutes(
); );
} }
}, },
onPressCreateGroupChat: () async => context.push( onPressCreateGroupChat: () async {
configuration.chatService.chatOverviewService
.clearCurrentlySelectedUsers();
return context.push(
ChatUserStoryRoutes.newGroupChatScreen, ChatUserStoryRoutes.newGroupChatScreen,
), );
},
); );
return buildScreenWithoutTransition( return buildScreenWithoutTransition(
context: context, context: context,
@ -211,25 +216,25 @@ List<GoRoute> getChatStoryRoutes(
pageBuilder: (context, state) { pageBuilder: (context, state) {
var service = configuration.chatServiceBuilder?.call(context) ?? var service = configuration.chatServiceBuilder?.call(context) ??
configuration.chatService; configuration.chatService;
var users = state.extra! as List<ChatUserModel>;
var newGroupChatOverviewScreen = NewGroupChatOverviewScreen( var newGroupChatOverviewScreen = NewGroupChatOverviewScreen(
options: configuration.chatOptionsBuilder(context), options: configuration.chatOptionsBuilder(context),
translations: configuration.translationsBuilder?.call(context) ?? translations: configuration.translationsBuilder?.call(context) ??
configuration.translations, configuration.translations,
service: service, service: service,
users: users, onPressCompleteGroupChatCreation:
onPressCompleteGroupChatCreation: (users, groupChatName) async { (users, groupChatName, groupBio, image) async {
configuration.onPressCompleteGroupChatCreation configuration.onPressCompleteGroupChatCreation
?.call(users, groupChatName); ?.call(users, groupChatName, image);
var chat = await configuration.chatService.chatOverviewService var chat = await configuration.chatService.chatOverviewService
.storeChatIfNot( .storeChatIfNot(
GroupChatModel( GroupChatModel(
canBeDeleted: true, canBeDeleted: true,
title: groupChatName, title: groupChatName,
imageUrl: "https://picsum.photos/200/300",
users: users, users: users,
bio: groupBio,
), ),
image,
); );
if (context.mounted) { if (context.mounted) {
context.go( context.go(
@ -259,13 +264,21 @@ List<GoRoute> getChatStoryRoutes(
var id = userId == "null" ? null : userId; var id = userId == "null" ? null : userId;
var service = configuration.chatServiceBuilder?.call(context) ?? var service = configuration.chatServiceBuilder?.call(context) ??
configuration.chatService; configuration.chatService;
ChatUserModel? currentUser;
String? currentUserId;
Future.delayed(Duration.zero, () async {
currentUser = await service.chatUserService.getCurrentUser();
currentUserId = currentUser!.id;
});
var profileScreen = ChatProfileScreen( var profileScreen = ChatProfileScreen(
options: configuration.chatOptionsBuilder(context),
translations: configuration.translationsBuilder?.call(context) ?? translations: configuration.translationsBuilder?.call(context) ??
configuration.translations, configuration.translations,
chatService: service, chatService: service,
chatId: chatId!, chatId: chatId!,
userId: id, userId: id,
currentUserId: currentUserId!,
onTapUser: (user) async { onTapUser: (user) async {
if (configuration.onPressUserProfile != null) { if (configuration.onPressUserProfile != null) {
return configuration.onPressUserProfile!.call(context, user); return configuration.onPressUserProfile!.call(context, user);
@ -275,6 +288,26 @@ List<GoRoute> getChatStoryRoutes(
ChatUserStoryRoutes.chatProfileScreenPath(chatId, user.id), ChatUserStoryRoutes.chatProfileScreenPath(chatId, user.id),
); );
}, },
onPressStartChat: (user) async {
configuration.onPressCreateChat?.call(user);
if (configuration.onPressCreateChat != null) return;
var chat = await configuration.chatService.chatOverviewService
.getChatByUser(user);
if (chat.id == null) {
chat = await configuration.chatService.chatOverviewService
.storeChatIfNot(
PersonalChatModel(
user: user,
),
null,
);
}
if (context.mounted) {
await context.push(
ChatUserStoryRoutes.chatDetailViewPath(chat.id ?? ""),
);
}
},
); );
return buildScreenWithoutTransition( return buildScreenWithoutTransition(
context: context, context: context,

View file

@ -83,7 +83,11 @@ class ChatUserStoryConfiguration {
final Function(ChatUserModel)? onPressCreateChat; final Function(ChatUserModel)? onPressCreateChat;
/// Builder for chat options based on context. /// Builder for chat options based on context.
final Function(List<ChatUserModel>, String)? onPressCompleteGroupChatCreation; final Function(
List<ChatUserModel> users,
String groupchatName,
Uint8List? image,
)? onPressCompleteGroupChatCreation;
final Function()? onPressCreateGroupChat; final Function()? onPressCreateGroupChat;

View file

@ -19,6 +19,7 @@ class FirebaseChatDocument {
this.title, this.title,
this.imageUrl, this.imageUrl,
this.lastMessage, this.lastMessage,
this.bio,
}); });
/// Constructs a FirebaseChatDocument from JSON. /// Constructs a FirebaseChatDocument from JSON.
@ -34,7 +35,8 @@ class FirebaseChatDocument {
: FirebaseMessageDocument.fromJson( : FirebaseMessageDocument.fromJson(
json["last_message"], json["last_message"],
null, null,
); ),
bio = json["bio"];
/// The unique identifier of the chat document. /// The unique identifier of the chat document.
final String? id; final String? id;
@ -60,6 +62,8 @@ class FirebaseChatDocument {
/// The last message in the chat. /// The last message in the chat.
final FirebaseMessageDocument? lastMessage; final FirebaseMessageDocument? lastMessage;
final String? bio;
/// Converts the FirebaseChatDocument to JSON format. /// Converts the FirebaseChatDocument to JSON format.
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"title": title, "title": title,
@ -68,5 +72,6 @@ class FirebaseChatDocument {
"last_used": lastUsed, "last_used": lastUsed,
"can_be_deleted": canBeDeleted, "can_be_deleted": canBeDeleted,
"users": users, "users": users,
"bio": bio,
}; };
} }

View file

@ -241,7 +241,6 @@ class FirebaseChatDetailService
_cumulativeMessages = []; _cumulativeMessages = [];
lastChat = chatId; lastChat = chatId;
lastMessage = null; lastMessage = null;
debugPrint("Canceling messages stream");
}, },
); );

View file

@ -4,16 +4,20 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import "dart:async"; import "dart:async";
import "dart:typed_data";
import "package:cloud_firestore/cloud_firestore.dart"; import "package:cloud_firestore/cloud_firestore.dart";
import "package:firebase_core/firebase_core.dart"; import "package:firebase_core/firebase_core.dart";
import "package:firebase_storage/firebase_storage.dart"; import "package:firebase_storage/firebase_storage.dart";
import "package:flutter/material.dart";
import "package:flutter_chat_firebase/config/firebase_chat_options.dart"; import "package:flutter_chat_firebase/config/firebase_chat_options.dart";
import "package:flutter_chat_firebase/dto/firebase_chat_document.dart"; import "package:flutter_chat_firebase/dto/firebase_chat_document.dart";
import "package:flutter_chat_interface/flutter_chat_interface.dart"; import "package:flutter_chat_interface/flutter_chat_interface.dart";
/// Service class for managing chat overviews using Firebase. /// Service class for managing chat overviews using Firebase.
class FirebaseChatOverviewService implements ChatOverviewService { class FirebaseChatOverviewService
with ChangeNotifier
implements ChatOverviewService {
late FirebaseFirestore _db; late FirebaseFirestore _db;
late FirebaseStorage _storage; late FirebaseStorage _storage;
late ChatUserService _userService; late ChatUserService _userService;
@ -38,6 +42,8 @@ class FirebaseChatOverviewService implements ChatOverviewService {
_options = options ?? const FirebaseChatOptions(); _options = options ?? const FirebaseChatOptions();
} }
final List<ChatUserModel> _currentlySelectedUsers = [];
Future<int?> _addUnreadChatSubscription( Future<int?> _addUnreadChatSubscription(
String chatId, String chatId,
String userId, String userId,
@ -285,6 +291,7 @@ class FirebaseChatOverviewService implements ChatOverviewService {
imageUrl: chat?.imageUrl ?? "", imageUrl: chat?.imageUrl ?? "",
users: users, users: users,
canBeDeleted: chat?.canBeDeleted ?? true, canBeDeleted: chat?.canBeDeleted ?? true,
bio: chat?.bio,
); );
} }
} }
@ -338,7 +345,7 @@ class FirebaseChatOverviewService implements ChatOverviewService {
/// ///
/// [chat]: The chat to be stored. /// [chat]: The chat to be stored.
@override @override
Future<ChatModel> storeChatIfNot(ChatModel chat) async { Future<ChatModel> storeChatIfNot(ChatModel chat, Uint8List? image) async {
if (chat.id == null) { if (chat.id == null) {
var currentUser = await _userService.getCurrentUser(); var currentUser = await _userService.getCurrentUser();
if (chat is PersonalChatModel) { if (chat is PersonalChatModel) {
@ -398,22 +405,41 @@ class FirebaseChatOverviewService implements ChatOverviewService {
FirebaseChatDocument( FirebaseChatDocument(
personal: false, personal: false,
title: chat.title, title: chat.title,
imageUrl: chat.imageUrl,
canBeDeleted: chat.canBeDeleted, canBeDeleted: chat.canBeDeleted,
users: userIds, users: userIds,
lastUsed: Timestamp.now(), lastUsed: Timestamp.now(),
bio: chat.bio,
), ),
); );
if (image != null) {
var imageUrl = await uploadGroupChatImage(image, reference.id);
chat.copyWith(imageUrl: imageUrl);
await _db
.collection(_options.chatsMetaDataCollectionName)
.doc(reference.id)
.set({"image_url": imageUrl}, SetOptions(merge: true));
}
var currentChat = await _db
.collection(_options.chatsMetaDataCollectionName)
.doc(reference.id)
.withConverter(
fromFirestore: (snapshot, _) =>
FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (chat, _) => chat.toJson(),
)
.get();
for (var userId in userIds) { for (var userId in userIds) {
await _db await _db
.collection(_options.usersCollectionName) .collection(_options.usersCollectionName)
.doc(userId) .doc(userId)
.collection(_options.groupChatsCollectionName) .collection(_options.groupChatsCollectionName)
.doc(reference.id) .doc(currentChat.id)
.set({"users": userIds}, SetOptions(merge: true)); .set({"users": userIds}, SetOptions(merge: true));
} }
chat.id = reference.id; chat.id = reference.id;
currentlySelectedUsers.clear();
} else { } else {
throw Exception("Chat type not supported for firebase"); throw Exception("Chat type not supported for firebase");
} }
@ -478,4 +504,34 @@ class FirebaseChatOverviewService implements ChatOverviewService {
.doc(chat.id) .doc(chat.id)
.set({"amount_unread_messages": 0}, SetOptions(merge: true)); .set({"amount_unread_messages": 0}, SetOptions(merge: true));
} }
@override
List<ChatUserModel> get currentlySelectedUsers => _currentlySelectedUsers;
@override
void addCurrentlySelectedUser(ChatUserModel user) {
_currentlySelectedUsers.add(user);
notifyListeners();
}
@override
void removeCurrentlySelectedUser(ChatUserModel user) {
_currentlySelectedUsers.remove(user);
notifyListeners();
}
@override
Future<String> uploadGroupChatImage(Uint8List image, String chatId) async {
await _storage.ref("groupchatImages/$chatId").putData(image);
var imageUrl =
await _storage.ref("groupchatImages/$chatId").getDownloadURL();
return imageUrl;
}
@override
void clearCurrentlySelectedUsers() {
_currentlySelectedUsers.clear();
notifyListeners();
}
} }

View file

@ -4,7 +4,7 @@
name: flutter_chat_firebase name: flutter_chat_firebase
description: A new Flutter package project. description: A new Flutter package project.
version: 3.0.1 version: 3.1.0
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub

View file

@ -16,8 +16,9 @@ abstract class GroupChatModelInterface extends ChatModel {
}); });
String get title; String get title;
String get imageUrl; String? get imageUrl;
List<ChatUserModel> get users; List<ChatUserModel> get users;
String? get bio;
@override @override
GroupChatModelInterface copyWith({ GroupChatModelInterface copyWith({
@ -30,6 +31,7 @@ abstract class GroupChatModelInterface extends ChatModel {
String? imageUrl, String? imageUrl,
List<ChatUserModel>? users, List<ChatUserModel>? users,
bool? canBeDeleted, bool? canBeDeleted,
String? bio,
}); });
} }
@ -56,13 +58,14 @@ class GroupChatModel implements GroupChatModelInterface {
GroupChatModel({ GroupChatModel({
required this.canBeDeleted, required this.canBeDeleted,
required this.title, required this.title,
required this.imageUrl,
required this.users, required this.users,
this.imageUrl,
this.id, this.id,
this.messages, this.messages,
this.unreadMessages, this.unreadMessages,
this.lastUsed, this.lastUsed,
this.lastMessage, this.lastMessage,
this.bio,
}); });
@override @override
@ -80,9 +83,11 @@ class GroupChatModel implements GroupChatModelInterface {
@override @override
final String title; final String title;
@override @override
final String imageUrl; final String? imageUrl;
@override @override
final List<ChatUserModel> users; final List<ChatUserModel> users;
@override
final String? bio;
@override @override
GroupChatModel copyWith({ GroupChatModel copyWith({
@ -95,6 +100,7 @@ class GroupChatModel implements GroupChatModelInterface {
String? title, String? title,
String? imageUrl, String? imageUrl,
List<ChatUserModel>? users, List<ChatUserModel>? users,
String? bio,
}) => }) =>
GroupChatModel( GroupChatModel(
id: id ?? this.id, id: id ?? this.id,
@ -106,5 +112,6 @@ class GroupChatModel implements GroupChatModelInterface {
title: title ?? this.title, title: title ?? this.title,
imageUrl: imageUrl ?? this.imageUrl, imageUrl: imageUrl ?? this.imageUrl,
users: users ?? this.users, users: users ?? this.users,
bio: bio ?? this.bio,
); );
} }

View file

@ -1,6 +1,9 @@
import "dart:typed_data";
import "package:flutter/material.dart";
import "package:flutter_chat_interface/flutter_chat_interface.dart"; import "package:flutter_chat_interface/flutter_chat_interface.dart";
abstract class ChatOverviewService { abstract class ChatOverviewService extends ChangeNotifier {
/// Retrieves a stream of chats. /// Retrieves a stream of chats.
/// This stream is updated whenever a new chat is created. /// This stream is updated whenever a new chat is created.
Stream<List<ChatModel>> getChatsStream(); Stream<List<ChatModel>> getChatsStream();
@ -18,8 +21,22 @@ abstract class ChatOverviewService {
Future<void> readChat(ChatModel chat); Future<void> readChat(ChatModel chat);
/// Creates the chat if it does not exist. /// Creates the chat if it does not exist.
Future<ChatModel> storeChatIfNot(ChatModel chat); Future<ChatModel> storeChatIfNot(ChatModel chat, Uint8List? image);
/// Retrieves the number of unread chats. /// Retrieves the number of unread chats.
Stream<int> getUnreadChatsCountStream(); Stream<int> getUnreadChatsCountStream();
/// Retrieves the currently selected users to be added to a new groupchat.
List<ChatUserModel> get currentlySelectedUsers;
/// Adds a user to the currently selected users.
void addCurrentlySelectedUser(ChatUserModel user);
/// Deletes a user from the currently selected users.
void removeCurrentlySelectedUser(ChatUserModel user);
void clearCurrentlySelectedUsers();
/// uploads an image for a group chat.
Future<String> uploadGroupChatImage(Uint8List image, String chatId);
} }

View file

@ -4,7 +4,7 @@
name: flutter_chat_interface name: flutter_chat_interface
description: A new Flutter package project. description: A new Flutter package project.
version: 3.0.1 version: 3.1.0
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
environment: environment:

View file

@ -7,6 +7,9 @@ import "package:flutter_chat_interface/flutter_chat_interface.dart";
class LocalChatOverviewService class LocalChatOverviewService
with ChangeNotifier with ChangeNotifier
implements ChatOverviewService { implements ChatOverviewService {
/// The list of currently selected users.
final List<ChatUserModel> _currentlySelectedUsers = [];
/// The list of personal chat models. /// The list of personal chat models.
final List<ChatModel> _chats = []; final List<ChatModel> _chats = [];
@ -22,7 +25,6 @@ class LocalChatOverviewService
_chats[index] = chat; _chats[index] = chat;
_chatsController.addStream(Stream.value(_chats)); _chatsController.addStream(Stream.value(_chats));
notifyListeners(); notifyListeners();
debugPrint("Chat updated: $chat");
return Future.value(); return Future.value();
} }
@ -31,15 +33,12 @@ class LocalChatOverviewService
_chats.removeWhere((element) => element.id == chat.id); _chats.removeWhere((element) => element.id == chat.id);
_chatsController.add(_chats); _chatsController.add(_chats);
notifyListeners(); notifyListeners();
debugPrint("Chat deleted: $chat");
return Future.value(); return Future.value();
} }
@override @override
Future<ChatModel> getChatById(String id) { Future<ChatModel> getChatById(String id) {
var chat = _chats.firstWhere((element) => element.id == id); var chat = _chats.firstWhere((element) => element.id == id);
debugPrint("Retrieved chat by ID: $chat");
debugPrint("Messages are: ${chat.messages?.length}");
return Future.value(chat); return Future.value(chat);
} }
@ -59,7 +58,6 @@ class LocalChatOverviewService
); );
chat.id = chat.hashCode.toString(); chat.id = chat.hashCode.toString();
_chats.add(chat); _chats.add(chat);
debugPrint("New chat created: $chat");
} }
_chatsController.add([..._chats]); _chatsController.add([..._chats]);
@ -77,19 +75,42 @@ class LocalChatOverviewService
Future<void> readChat(ChatModel chat) async => Future.value(); Future<void> readChat(ChatModel chat) async => Future.value();
@override @override
Future<ChatModel> storeChatIfNot(ChatModel chat) { Future<ChatModel> storeChatIfNot(ChatModel chat, Uint8List? image) {
var chatExists = _chats.any((element) => element.id == chat.id); var chatExists = _chats.any((element) => element.id == chat.id);
if (!chatExists) { if (!chatExists) {
chat.id = chat.hashCode.toString(); chat.id = chat.hashCode.toString();
_chats.add(chat); _chats.add(chat);
_chatsController.add([..._chats]); _chatsController.add([..._chats]);
currentlySelectedUsers.clear();
notifyListeners(); notifyListeners();
debugPrint("Chat stored: $chat");
} else {
debugPrint("Chat already exists: $chat");
} }
return Future.value(chat); return Future.value(chat);
} }
@override
List<ChatUserModel> get currentlySelectedUsers => _currentlySelectedUsers;
@override
void addCurrentlySelectedUser(ChatUserModel user) {
_currentlySelectedUsers.add(user);
notifyListeners();
}
@override
void removeCurrentlySelectedUser(ChatUserModel user) {
_currentlySelectedUsers.remove(user);
notifyListeners();
}
@override
Future<String> uploadGroupChatImage(Uint8List image, String chatId) =>
Future.value("https://picsum.photos/200/300");
@override
void clearCurrentlySelectedUsers() {
_currentlySelectedUsers.clear();
notifyListeners();
}
} }

View file

@ -30,7 +30,7 @@ class LocalChatUserService implements ChatUserService {
@override @override
Future<ChatUserModel?> getCurrentUser() => Future<ChatUserModel?> getCurrentUser() =>
Future.value(const ChatUserModel()); Future.value(users.where((element) => element.id == "3").first);
@override @override
Future<ChatUserModel?> getUser(String id) { Future<ChatUserModel?> getUser(String id) {

View file

@ -1,6 +1,6 @@
name: flutter_chat_local name: flutter_chat_local
description: "A new Flutter package project." description: "A new Flutter package project."
version: 3.0.1 version: 3.1.0
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub

View file

@ -103,11 +103,7 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
Text( Text(
widget.message.sender.fullName ?? widget.message.sender.fullName ??
widget.translations.anonymousUser, widget.translations.anonymousUser,
style: TextStyle( style: theme.textTheme.titleMedium,
fontSize: 16,
fontWeight: FontWeight.w800,
color: theme.textTheme.labelMedium?.color,
),
), ),
), ),
Padding( Padding(
@ -115,13 +111,9 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
child: Text( child: Text(
_dateFormatter.format( _dateFormatter.format(
date: widget.message.timestamp, date: widget.message.timestamp,
showFullDate: true, showFullDate: false,
),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w300,
color: Color(0xFFBBBBBB),
), ),
style: theme.textTheme.labelSmall,
), ),
), ),
], ],
@ -137,10 +129,7 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
Flexible( Flexible(
child: Text( child: Text(
(widget.message as ChatTextMessageModel).text, (widget.message as ChatTextMessageModel).text,
style: TextStyle( style: theme.textTheme.bodySmall,
fontSize: 16,
color: theme.textTheme.labelMedium?.color,
),
), ),
), ),
if (widget.showTime && if (widget.showTime &&
@ -155,7 +144,7 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
) )
.split(" ") .split(" ")
.last, .last,
style: theme.textTheme.bodySmall, style: theme.textTheme.labelSmall,
textAlign: TextAlign.end, textAlign: TextAlign.end,
), ),
], ],

View file

@ -49,9 +49,7 @@ class ChatRow extends StatelessWidget {
title, title,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: unreadMessages > 0 style: theme.textTheme.titleMedium,
? theme.textTheme.bodyLarge
: theme.textTheme.bodyMedium,
), ),
if (subTitle != null) ...[ if (subTitle != null) ...[
Padding( Padding(
@ -59,8 +57,10 @@ class ChatRow extends StatelessWidget {
child: Text( child: Text(
subTitle!, subTitle!,
style: unreadMessages > 0 style: unreadMessages > 0
? theme.textTheme.bodyLarge ? theme.textTheme.bodySmall!.copyWith(
: theme.textTheme.bodyMedium, fontWeight: FontWeight.w800,
)
: theme.textTheme.bodySmall,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 2, maxLines: 2,
), ),
@ -79,10 +79,7 @@ class ChatRow extends StatelessWidget {
padding: const EdgeInsets.only(bottom: 4.0), padding: const EdgeInsets.only(bottom: 4.0),
child: Text( child: Text(
lastUsed!, lastUsed!,
style: const TextStyle( style: theme.textTheme.labelSmall,
color: Color(0xFFBBBBBB),
fontSize: 14,
),
), ),
), ),
], ],

View file

@ -0,0 +1,33 @@
import "dart:typed_data";
import "package:flutter/material.dart";
import "package:flutter_chat_view/flutter_chat_view.dart";
import "package:flutter_chat_view/src/components/image_loading_snackbar.dart";
Future<void> onPressSelectImage(
BuildContext context,
ChatTranslations translations,
ChatOptions options,
Function(Uint8List image) onUploadImage,
) async =>
showModalBottomSheet<Uint8List?>(
context: context,
builder: (BuildContext context) => options.imagePickerContainerBuilder(
() => Navigator.of(context).pop(),
translations,
context,
),
).then(
(image) async {
if (image == null) return;
var messenger = ScaffoldMessenger.of(context)
..showSnackBar(
getImageLoadingSnackbar(translations),
)
..activate();
await onUploadImage(image);
Future.delayed(const Duration(seconds: 1), () {
messenger.hideCurrentSnackBar();
});
},
);

View file

@ -4,7 +4,6 @@
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/components/chat_image.dart";
import "package:flutter_image_picker/flutter_image_picker.dart"; import "package:flutter_image_picker/flutter_image_picker.dart";
import "package:flutter_profile/flutter_profile.dart"; import "package:flutter_profile/flutter_profile.dart";
@ -57,15 +56,16 @@ Widget _createNewChatButton(
BuildContext context, BuildContext context,
VoidCallback onPressed, VoidCallback onPressed,
ChatTranslations translations, ChatTranslations translations,
) => ) {
Padding( var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 24, vertical: 24,
horizontal: 5, horizontal: 4,
), ),
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: theme.colorScheme.primary,
fixedSize: const Size(254, 44), fixedSize: const Size(254, 44),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(56), borderRadius: BorderRadius.circular(56),
@ -74,14 +74,11 @@ Widget _createNewChatButton(
onPressed: onPressed, onPressed: onPressed,
child: Text( child: Text(
translations.newChatButton, translations.newChatButton,
style: const TextStyle( style: theme.textTheme.displayLarge,
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 20,
),
), ),
), ),
); );
}
Widget _createMessageInput( Widget _createMessageInput(
TextEditingController textEditingController, TextEditingController textEditingController,
@ -91,6 +88,7 @@ Widget _createMessageInput(
) { ) {
var theme = Theme.of(context); var theme = Theme.of(context);
return TextField( return TextField(
style: theme.textTheme.bodySmall,
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
controller: textEditingController, controller: textEditingController,
decoration: InputDecoration( decoration: InputDecoration(
@ -111,7 +109,9 @@ Widget _createMessageInput(
horizontal: 30, horizontal: 30,
), ),
hintText: translations.messagePlaceholder, hintText: translations.messagePlaceholder,
hintStyle: theme.inputDecorationTheme.hintStyle, hintStyle: theme.textTheme.bodyMedium!.copyWith(
color: theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
),
fillColor: Colors.white, fillColor: Colors.white,
filled: true, filled: true,
border: const OutlineInputBorder( border: const OutlineInputBorder(
@ -127,24 +127,33 @@ Widget _createMessageInput(
Widget _createChatRowContainer( Widget _createChatRowContainer(
Widget chatRow, Widget chatRow,
) => BuildContext context,
Padding( ) {
padding: const EdgeInsets.symmetric( var theme = Theme.of(context);
vertical: 12.0, return DecoratedBox(
horizontal: 10.0, decoration: BoxDecoration(
),
child: ColoredBox(
color: Colors.transparent, color: Colors.transparent,
border: Border(
bottom: BorderSide(
color: theme.dividerColor,
width: 0.5,
),
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: chatRow, child: chatRow,
), ),
); );
}
Widget _createImagePickerContainer( Widget _createImagePickerContainer(
VoidCallback onClose, VoidCallback onClose,
ChatTranslations translations, ChatTranslations translations,
BuildContext context, BuildContext context,
) => ) {
Container( var theme = Theme.of(context);
return Container(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
color: Colors.white, color: Colors.white,
child: ImagePicker( child: ImagePicker(
@ -155,19 +164,23 @@ Widget _createImagePickerContainer(
iconSize: 60.0, iconSize: 60.0,
makePhotoText: translations.takePicture, makePhotoText: translations.takePicture,
selectImageText: translations.uploadFile, selectImageText: translations.uploadFile,
selectImageIcon: const Icon(
Icons.insert_drive_file_rounded,
size: 60,
), ),
customButton: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
), ),
customButton: TextButton(
onPressed: onClose, onPressed: onClose,
child: Text( child: Text(
translations.cancelImagePickerBtn, translations.cancelImagePickerBtn,
style: const TextStyle(color: Colors.white), style: theme.textTheme.bodyMedium!.copyWith(
decoration: TextDecoration.underline,
),
), ),
), ),
), ),
); );
}
Scaffold _createScaffold( Scaffold _createScaffold(
AppBar appbar, AppBar appbar,
@ -185,20 +198,27 @@ Widget _createUserAvatar(
double size, double size,
) => ) =>
Avatar( Avatar(
boxfit: BoxFit.cover,
user: User( user: User(
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
imageUrl: user.imageUrl, imageUrl: user.imageUrl != "" ? user.imageUrl : null,
), ),
size: size, size: size,
); );
Widget _createGroupAvatar( Widget _createGroupAvatar(
String groupName, String groupName,
String imageUrl, String? imageUrl,
double size, double size,
) => ) =>
ChatImage( Avatar(
image: imageUrl, boxfit: BoxFit.cover,
user: User(
firstName: groupName,
lastName: null,
imageUrl: imageUrl != "" ? imageUrl : null,
),
size: size, size: size,
); );
@ -249,6 +269,7 @@ typedef TextInputBuilder = Widget Function(
typedef ContainerBuilder = Widget Function( typedef ContainerBuilder = Widget Function(
Widget child, Widget child,
BuildContext context,
); );
typedef ImagePickerContainerBuilder = Widget Function( typedef ImagePickerContainerBuilder = Widget Function(
@ -270,7 +291,7 @@ typedef UserAvatarBuilder = Widget Function(
typedef GroupAvatarBuilder = Widget Function( typedef GroupAvatarBuilder = Widget Function(
String groupName, String groupName,
String imageUrl, String? imageUrl,
double size, double size,
); );

View file

@ -15,7 +15,6 @@ class ChatTranslations {
required this.newChatButton, required this.newChatButton,
required this.newGroupChatButton, required this.newGroupChatButton,
required this.newChatTitle, required this.newChatTitle,
required this.deleteChatButton,
required this.image, required this.image,
required this.searchPlaceholder, required this.searchPlaceholder,
required this.startTyping, required this.startTyping,
@ -40,6 +39,13 @@ class ChatTranslations {
required this.groupNameValidatorTooLong, required this.groupNameValidatorTooLong,
required this.groupNameHintText, required this.groupNameHintText,
required this.newGroupChatTitle, required this.newGroupChatTitle,
required this.groupBioHintText,
required this.groupProfileBioHeader,
required this.groupBioValidatorEmpty,
required this.groupChatNameFieldHeader,
required this.groupBioFieldHeader,
required this.selectedMembersHeader,
required this.createGroupChatButton,
}); });
/// Default translations for the chat component view /// Default translations for the chat component view
@ -47,7 +53,7 @@ class ChatTranslations {
this.chatsTitle = "Chats", this.chatsTitle = "Chats",
this.chatsUnread = "unread", this.chatsUnread = "unread",
this.newChatButton = "Start chat", this.newChatButton = "Start chat",
this.newGroupChatButton = "Create a group chat", this.newGroupChatButton = "Create a groupchat",
this.newChatTitle = "Start a chat", this.newChatTitle = "Start a chat",
this.image = "Image", this.image = "Image",
this.searchPlaceholder = "Search...", this.searchPlaceholder = "Search...",
@ -58,25 +64,31 @@ class ChatTranslations {
this.writeFirstMessageInGroupChat = this.writeFirstMessageInGroupChat =
"Write the first message in this group chat", "Write the first message in this group chat",
this.imageUploading = "Image is uploading...", this.imageUploading = "Image is uploading...",
this.deleteChatButton = "Delete",
this.deleteChatModalTitle = "Delete chat", this.deleteChatModalTitle = "Delete chat",
this.deleteChatModalDescription = this.deleteChatModalDescription =
"Are you sure you want to delete this chat?", "Are you sure you want to delete this chat?",
this.deleteChatModalCancel = "Cancel", this.deleteChatModalCancel = "Cancel",
this.deleteChatModalConfirm = "Delete", this.deleteChatModalConfirm = "Confirm",
this.noUsersFound = "No users were found to start a chat with", this.noUsersFound = "No users were found to start a chat with",
this.noChatsFound = "Click on 'Start a chat' to create a new chat", this.noChatsFound = "Click on 'Start a chat' to create a new chat",
this.anonymousUser = "Anonymous user", this.anonymousUser = "Anonymous user",
this.chatCantBeDeleted = "This chat can't be deleted", this.chatCantBeDeleted = "This chat can't be deleted",
this.chatProfileUsers = "Users:", this.chatProfileUsers = "Members:",
this.imagePickerTitle = "Do you want to upload a file or take a picture?", this.imagePickerTitle = "Do you want to upload a file or take a picture?",
this.uploadFile = "UPLOAD FILE", this.uploadFile = "UPLOAD FILE",
this.takePicture = "TAKE PICTURE", this.takePicture = "TAKE PICTURE",
this.groupNameHintText = "Group chat name", this.groupNameHintText = "Groupchat name",
this.groupNameValidatorEmpty = "Please enter a group chat name", this.groupNameValidatorEmpty = "Please enter a group chat name",
this.groupNameValidatorTooLong = this.groupNameValidatorTooLong =
"Group name is too long, max 15 characters", "Group name is too long, max 15 characters",
this.newGroupChatTitle = "New Group Chat", this.newGroupChatTitle = "start a groupchat",
this.groupBioHintText = "Bio",
this.groupProfileBioHeader = "Bio",
this.groupBioValidatorEmpty = "Please enter a bio",
this.groupChatNameFieldHeader = "Chat name",
this.groupBioFieldHeader = "Additional information for members",
this.selectedMembersHeader = "Members: ",
this.createGroupChatButton = "Create groupchat",
}); });
final String chatsTitle; final String chatsTitle;
@ -84,7 +96,6 @@ class ChatTranslations {
final String newChatButton; final String newChatButton;
final String newGroupChatButton; final String newGroupChatButton;
final String newChatTitle; final String newChatTitle;
final String deleteChatButton;
final String image; final String image;
final String searchPlaceholder; final String searchPlaceholder;
final String startTyping; final String startTyping;
@ -104,6 +115,10 @@ class ChatTranslations {
final String imagePickerTitle; final String imagePickerTitle;
final String uploadFile; final String uploadFile;
final String takePicture; final String takePicture;
final String groupChatNameFieldHeader;
final String groupBioFieldHeader;
final String selectedMembersHeader;
final String createGroupChatButton;
/// Shown when the user has no name /// Shown when the user has no name
final String anonymousUser; final String anonymousUser;
@ -111,6 +126,9 @@ class ChatTranslations {
final String groupNameValidatorTooLong; final String groupNameValidatorTooLong;
final String groupNameHintText; final String groupNameHintText;
final String newGroupChatTitle; final String newGroupChatTitle;
final String groupBioHintText;
final String groupProfileBioHeader;
final String groupBioValidatorEmpty;
// copyWith method to override the default values // copyWith method to override the default values
ChatTranslations copyWith({ ChatTranslations copyWith({
@ -119,7 +137,6 @@ class ChatTranslations {
String? newChatButton, String? newChatButton,
String? newGroupChatButton, String? newGroupChatButton,
String? newChatTitle, String? newChatTitle,
String? deleteChatButton,
String? image, String? image,
String? searchPlaceholder, String? searchPlaceholder,
String? startTyping, String? startTyping,
@ -144,6 +161,13 @@ class ChatTranslations {
String? groupNameValidatorTooLong, String? groupNameValidatorTooLong,
String? groupNameHintText, String? groupNameHintText,
String? newGroupChatTitle, String? newGroupChatTitle,
String? groupBioHintText,
String? groupProfileBioHeader,
String? groupBioValidatorEmpty,
String? groupChatNameFieldHeader,
String? groupBioFieldHeader,
String? selectedMembersHeader,
String? createGroupChatButton,
}) => }) =>
ChatTranslations( ChatTranslations(
chatsTitle: chatsTitle ?? this.chatsTitle, chatsTitle: chatsTitle ?? this.chatsTitle,
@ -151,7 +175,6 @@ class ChatTranslations {
newChatButton: newChatButton ?? this.newChatButton, newChatButton: newChatButton ?? this.newChatButton,
newGroupChatButton: newGroupChatButton ?? this.newGroupChatButton, newGroupChatButton: newGroupChatButton ?? this.newGroupChatButton,
newChatTitle: newChatTitle ?? this.newChatTitle, newChatTitle: newChatTitle ?? this.newChatTitle,
deleteChatButton: deleteChatButton ?? this.deleteChatButton,
image: image ?? this.image, image: image ?? this.image,
searchPlaceholder: searchPlaceholder ?? this.searchPlaceholder, searchPlaceholder: searchPlaceholder ?? this.searchPlaceholder,
startTyping: startTyping ?? this.startTyping, startTyping: startTyping ?? this.startTyping,
@ -177,9 +200,23 @@ class ChatTranslations {
uploadFile: uploadFile ?? this.uploadFile, uploadFile: uploadFile ?? this.uploadFile,
takePicture: takePicture ?? this.takePicture, takePicture: takePicture ?? this.takePicture,
anonymousUser: anonymousUser ?? this.anonymousUser, anonymousUser: anonymousUser ?? this.anonymousUser,
groupNameValidatorEmpty: this.groupNameValidatorEmpty, groupNameValidatorEmpty:
groupNameValidatorTooLong: this.groupNameValidatorTooLong, groupNameValidatorEmpty ?? this.groupNameValidatorEmpty,
groupNameHintText: this.groupNameHintText, groupNameValidatorTooLong:
newGroupChatTitle: this.newGroupChatTitle, groupNameValidatorTooLong ?? this.groupNameValidatorTooLong,
groupNameHintText: groupNameHintText ?? this.groupNameHintText,
newGroupChatTitle: newGroupChatTitle ?? this.newGroupChatTitle,
groupBioHintText: groupBioHintText ?? this.groupBioHintText,
groupProfileBioHeader:
groupProfileBioHeader ?? this.groupProfileBioHeader,
groupBioValidatorEmpty:
groupBioValidatorEmpty ?? this.groupBioValidatorEmpty,
groupChatNameFieldHeader:
groupChatNameFieldHeader ?? this.groupChatNameFieldHeader,
groupBioFieldHeader: groupBioFieldHeader ?? this.groupBioFieldHeader,
selectedMembersHeader:
selectedMembersHeader ?? this.selectedMembersHeader,
createGroupChatButton:
createGroupChatButton ?? this.createGroupChatButton,
); );
} }

View file

@ -9,7 +9,7 @@ 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/components/chat_bottom.dart"; import "package:flutter_chat_view/src/components/chat_bottom.dart";
import "package:flutter_chat_view/src/components/chat_detail_row.dart"; import "package:flutter_chat_view/src/components/chat_detail_row.dart";
import "package:flutter_chat_view/src/components/image_loading_snackbar.dart"; import "package:flutter_chat_view/src/components/image_picker_popup.dart";
class ChatDetailScreen extends StatefulWidget { class ChatDetailScreen extends StatefulWidget {
const ChatDetailScreen({ const ChatDetailScreen({
@ -139,29 +139,6 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
Future<void> onPressSelectImage() async => showModalBottomSheet<Uint8List?>(
context: context,
builder: (BuildContext context) =>
widget.options.imagePickerContainerBuilder(
() => Navigator.of(context).pop(),
widget.translations,
context,
),
).then(
(image) async {
if (image == null) return;
var messenger = ScaffoldMessenger.of(context)
..showSnackBar(
getImageLoadingSnackbar(widget.translations),
)
..activate();
await widget.onUploadImage(image);
Future.delayed(const Duration(seconds: 1), () {
messenger.hideCurrentSnackBar();
});
},
);
return FutureBuilder<ChatModel>( return FutureBuilder<ChatModel>(
// ignore: discarded_futures // ignore: discarded_futures
future: widget.service.chatOverviewService.getChatById(widget.chatId), future: widget.service.chatOverviewService.getChatById(widget.chatId),
@ -175,7 +152,6 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: theme.appBarTheme.backgroundColor,
iconTheme: theme.appBarTheme.iconTheme ?? iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white), const IconThemeData(color: Colors.white),
centerTitle: true, centerTitle: true,
@ -192,7 +168,7 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
child: widget.chatTitleBuilder?.call(chatTitle) ?? child: widget.chatTitleBuilder?.call(chatTitle) ??
Text( Text(
chatTitle, chatTitle,
style: theme.appBarTheme.titleTextStyle, style: theme.textTheme.headlineLarge,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@ -253,7 +229,12 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
ChatBottom( ChatBottom(
chat: chatModel, chat: chatModel,
messageInputBuilder: widget.options.messageInputBuilder, messageInputBuilder: widget.options.messageInputBuilder,
onPressSelectImage: onPressSelectImage, onPressSelectImage: () async => onPressSelectImage.call(
context,
widget.translations,
widget.options,
widget.onUploadImage,
),
onMessageSubmit: widget.onMessageSubmit, onMessageSubmit: widget.onMessageSubmit,
translations: widget.translations, translations: widget.translations,
iconColor: widget.iconColor, iconColor: widget.iconColor,

View file

@ -8,6 +8,9 @@ class ChatProfileScreen extends StatefulWidget {
required this.chatId, required this.chatId,
required this.translations, required this.translations,
required this.onTapUser, required this.onTapUser,
required this.options,
required this.onPressStartChat,
required this.currentUserId,
this.userId, this.userId,
super.key, super.key,
}); });
@ -27,6 +30,15 @@ class ChatProfileScreen extends StatefulWidget {
/// Callback function for tapping on a user. /// Callback function for tapping on a user.
final Function(ChatUserModel user) onTapUser; final Function(ChatUserModel user) onTapUser;
/// Chat options.
final ChatOptions options;
/// Callback function for starting a chat.
final Function(ChatUserModel user) onPressStartChat;
/// The current user.
final String currentUserId;
@override @override
State<ChatProfileScreen> createState() => _ProfileScreenState(); State<ChatProfileScreen> createState() => _ProfileScreenState();
} }
@ -65,9 +77,9 @@ class _ProfileScreenState extends State<ChatProfileScreen> {
imageUrl: data.imageUrl, imageUrl: data.imageUrl,
); );
} }
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: theme.appBarTheme.backgroundColor,
iconTheme: theme.appBarTheme.iconTheme ?? iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white), const IconThemeData(color: Colors.white),
title: Text( title: Text(
@ -78,16 +90,27 @@ class _ProfileScreenState extends State<ChatProfileScreen> {
: (data is GroupChatModel) : (data is GroupChatModel)
? data.title ? data.title
: "", : "",
style: theme.appBarTheme.titleTextStyle, style: theme.textTheme.headlineLarge,
), ),
), ),
body: snapshot.hasData body: snapshot.hasData
? ListView( ? Stack(
children: [
ListView(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 20), padding: const EdgeInsets.symmetric(vertical: 20),
child: Avatar( child: Column(
user: user, children: [
widget.options.userAvatarBuilder(
ChatUserModel(
firstName: user!.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl,
),
60,
),
],
), ),
), ),
const Divider( const Divider(
@ -97,44 +120,93 @@ class _ProfileScreenState extends State<ChatProfileScreen> {
if (data is GroupChatModel) ...[ if (data is GroupChatModel) ...[
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 100, vertical: 24,
vertical: 20, horizontal: 20,
), ),
child: Text(
widget.translations.chatProfileUsers,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
...data.users.map((e) {
var user = User(
firstName: e.firstName ?? "",
lastName: e.lastName ?? "",
imageUrl: e.imageUrl,
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: GestureDetector(
onTap: () {
widget.onTapUser.call(e);
},
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [ children: [
Avatar( Text(
user: user, widget.translations.groupProfileBioHeader,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 12,
), ),
Text( Text(
user.firstName!, data.bio ?? "",
style: theme.textTheme.bodyMedium!
.copyWith(color: Colors.black),
),
const SizedBox(
height: 12,
),
Text(
widget.translations.chatProfileUsers,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 12,
),
Wrap(
children: [
...data.users.map(
(user) => Padding(
padding: const EdgeInsets.only(
bottom: 8,
right: 8,
),
child: GestureDetector(
onTap: () {
widget.onTapUser.call(user);
},
child: Column(
mainAxisAlignment:
MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
widget.options.userAvatarBuilder(
user,
44,
), ),
], ],
), ),
), ),
); ),
}), ),
],
),
],
),
),
],
],
),
if (data is ChatUserModel &&
widget.currentUserId != data.id) ...[
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 80,
),
child: FilledButton(
onPressed: () {
widget.onPressStartChat(data);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.translations.newChatButton,
style: theme.textTheme.displayLarge,
),
],
),
),
),
),
], ],
], ],
) )

View file

@ -77,10 +77,9 @@ class _ChatScreenState extends State<ChatScreen> {
var theme = Theme.of(context); var theme = Theme.of(context);
return widget.options.scaffoldBuilder( return widget.options.scaffoldBuilder(
AppBar( AppBar(
backgroundColor: theme.appBarTheme.backgroundColor,
title: Text( title: Text(
translations.chatsTitle, translations.chatsTitle,
style: theme.appBarTheme.titleTextStyle, style: theme.textTheme.headlineLarge,
), ),
centerTitle: true, centerTitle: true,
actions: [ actions: [
@ -96,9 +95,8 @@ class _ChatScreenState extends State<ChatScreen> {
child: Text( child: Text(
"${snapshot.data ?? 0} ${translations.chatsUnread}", "${snapshot.data ?? 0} ${translations.chatsUnread}",
style: widget.unreadMessageTextStyle ?? style: widget.unreadMessageTextStyle ??
const TextStyle( theme.textTheme.bodySmall!.copyWith(
color: Colors.white, color: Colors.white,
fontSize: 14,
), ),
), ),
), ),
@ -147,119 +145,23 @@ class _ChatScreenState extends State<ChatScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
bottom: BorderSide( bottom: BorderSide(
color: theme.colorScheme.secondary color: theme.dividerColor,
.withOpacity(0.3),
width: 0.5, width: 0.5,
), ),
), ),
), ),
child: Builder( child: Builder(
builder: (context) => !(widget builder: (context) =>
.disableDismissForPermanentChats && !(widget.disableDismissForPermanentChats &&
!chat.canBeDeleted) !chat.canBeDeleted)
? Dismissible( ? Dismissible(
confirmDismiss: (_) async => confirmDismiss: (_) async =>
widget.deleteChatDialog widget.deleteChatDialog
?.call(context, chat) ?? ?.call(context, chat) ??
showModalBottomSheet( _deleteDialog(
context: context, chat,
builder: (BuildContext context) => translations,
Container(
padding:
const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
Text(
chat.canBeDeleted
? translations
.deleteChatModalTitle
: translations
.chatCantBeDeleted,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 24,
fontWeight:
FontWeight.bold,
),
),
const SizedBox(height: 24),
if (chat.canBeDeleted)
Padding(
padding: const EdgeInsets
.symmetric(
horizontal: 16,
),
child: Text(
translations
.deleteChatModalDescription,
textAlign:
TextAlign.center,
style: const TextStyle(
fontSize: 18,
),
),
),
const SizedBox(
height: 24,
),
Row(
mainAxisAlignment:
MainAxisAlignment
.center,
children: [
ElevatedButton(
onPressed: () =>
Navigator.of(
context, context,
).pop(false),
child: Text(
translations
.deleteChatModalCancel,
style:
const TextStyle(
color: Colors.black,
fontSize: 18,
),
),
),
if (chat.canBeDeleted)
const SizedBox(
width: 16,
),
if (chat.canBeDeleted)
ElevatedButton(
style: ElevatedButton
.styleFrom(
backgroundColor:
Theme.of(
context,
).primaryColor,
),
onPressed: () =>
Navigator.of(
context,
).pop(
true,
),
child: Text(
translations
.deleteChatModalConfirm,
style:
const TextStyle(
color:
Colors.white,
fontSize: 18,
),
),
),
],
),
],
),
),
), ),
onDismissed: (_) { onDismissed: (_) {
setState(() { setState(() {
@ -267,14 +169,28 @@ class _ChatScreenState extends State<ChatScreen> {
}); });
widget.onDeleteChat(chat); widget.onDeleteChat(chat);
}, },
background: ColoredBox( secondaryBackground: const ColoredBox(
color: Colors.red, color: Colors.red,
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: Text( child: Icon(
translations.deleteChatButton, Icons.delete,
color: Colors.white,
),
),
),
),
background: const ColoredBox(
color: Colors.red,
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.delete,
color: Colors.white,
), ),
), ),
), ),
@ -318,6 +234,79 @@ class _ChatScreenState extends State<ChatScreen> {
theme.colorScheme.surface, theme.colorScheme.surface,
); );
} }
Future<bool?> _deleteDialog(
ChatModel chat,
ChatTranslations translations,
BuildContext context,
) async {
var theme = Theme.of(context);
var title = chat.canBeDeleted
? translations.deleteChatModalTitle
: translations.chatCantBeDeleted;
return showModalBottomSheet<bool>(
context: context,
builder: (BuildContext context) => Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
textAlign: TextAlign.center,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 20,
),
if (chat.canBeDeleted) ...[
Text(
translations.deleteChatModalDescription,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium,
),
const SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: FilledButton(
onPressed: () {
Navigator.of(
context,
).pop(true);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.translations.deleteChatModalConfirm,
style: theme.textTheme.displayLarge,
),
],
),
),
),
],
TextButton(
onPressed: () {
Navigator.of(
context,
).pop(false);
},
child: Text(
widget.translations.deleteChatModalCancel,
style: theme.textTheme.bodyMedium!.copyWith(
color: theme.textTheme.bodyMedium!.color?.withOpacity(0.5),
decoration: TextDecoration.underline,
),
),
),
],
),
),
);
}
} }
class ChatListItem extends StatelessWidget { class ChatListItem extends StatelessWidget {
@ -381,6 +370,7 @@ class ChatListItem extends StatelessWidget {
) )
: null, : null,
), ),
context,
), ),
); );
} }

View file

@ -50,7 +50,6 @@ class _NewChatScreenState extends State<NewChatScreen> {
appBar: AppBar( appBar: AppBar(
iconTheme: theme.appBarTheme.iconTheme ?? iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white), const IconThemeData(color: Colors.white),
backgroundColor: theme.appBarTheme.backgroundColor,
title: _buildSearchField(), title: _buildSearchField(),
actions: [ actions: [
_buildSearchIcon(), _buildSearchIcon(),
@ -58,47 +57,32 @@ class _NewChatScreenState extends State<NewChatScreen> {
), ),
body: Column( body: Column(
children: [ children: [
if (widget.showGroupChatButton) ...[ if (widget.showGroupChatButton && !_isSearching) ...[
GestureDetector(
onTap: () async {
await widget.onPressCreateGroupChat();
},
child: Container(
color: Colors.grey[900],
child: SizedBox(
height: 60.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 16.0, left: 32,
right: 32,
top: 20,
), ),
child: IconButton( child: FilledButton(
icon: const Icon( onPressed: () async {
Icons.group, await widget.onPressCreateGroupChat();
color: Colors.white,
),
onPressed: () {
// Handle group chat creation
}, },
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.groups,
), ),
const SizedBox(
width: 4,
), ),
Text( Text(
widget.translations.newGroupChatButton, widget.translations.newGroupChatButton,
style: const TextStyle( style: theme.textTheme.displayLarge,
color: Colors.white,
fontSize: 16.0,
),
), ),
], ],
), ),
],
),
),
), ),
), ),
], ],
@ -138,19 +122,20 @@ class _NewChatScreenState extends State<NewChatScreen> {
}, },
decoration: InputDecoration( decoration: InputDecoration(
hintText: widget.translations.searchPlaceholder, hintText: widget.translations.searchPlaceholder,
hintStyle: theme.inputDecorationTheme.hintStyle, hintStyle:
theme.textTheme.bodyMedium!.copyWith(color: Colors.white),
focusedBorder: UnderlineInputBorder( focusedBorder: UnderlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
), ),
), ),
style: theme.inputDecorationTheme.hintStyle, style: theme.textTheme.bodySmall!.copyWith(color: Colors.white),
cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white, cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white,
) )
: Text( : Text(
widget.translations.newChatTitle, widget.translations.newChatTitle,
style: theme.appBarTheme.titleTextStyle, style: theme.textTheme.headlineLarge,
); );
} }
@ -205,51 +190,14 @@ class _NewChatScreenState extends State<NewChatScreen> {
.noUsersPlaceholderBuilder(widget.translations, context); .noUsersPlaceholderBuilder(widget.translations, context);
} }
var isPressed = false; var isPressed = false;
return ListView.builder( return Padding(
padding: widget.options.paddingAroundChatList ??
const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
child: ListView.builder(
itemCount: filteredUsers.length, itemCount: filteredUsers.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
var user = filteredUsers[index]; var user = filteredUsers[index];
return DecoratedBox( return InkWell(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: theme.colorScheme.secondary.withOpacity(0.3),
width: 0.5,
),
),
),
child: GestureDetector(
child: widget.options.chatRowContainerBuilder(
Padding(
padding: widget.options.paddingAroundChatList ??
const EdgeInsets.symmetric(vertical: 8, horizontal: 28),
child: ColoredBox(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: widget.options.userAvatarBuilder(user, 40.0),
),
Expanded(
child: Container(
height: 40.0,
alignment: Alignment.centerLeft,
child: Text(
user.fullName ??
widget.translations.anonymousUser,
style: theme.textTheme.bodyLarge,
),
),
),
],
),
),
),
),
),
onTap: () async { onTap: () async {
if (!isPressed) { if (!isPressed) {
isPressed = true; isPressed = true;
@ -257,9 +205,24 @@ class _NewChatScreenState extends State<NewChatScreen> {
isPressed = false; isPressed = false;
} }
}, },
child: widget.options.chatRowContainerBuilder(
Row(
children: [
widget.options.userAvatarBuilder(user, 44),
const SizedBox(
width: 12,
),
Text(
user.fullName ?? widget.translations.anonymousUser,
style: theme.textTheme.titleMedium,
),
],
),
context,
), ),
); );
}, },
),
); );
} }
} }

View file

@ -2,15 +2,17 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import "dart:typed_data";
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/components/image_picker_popup.dart";
class NewGroupChatOverviewScreen extends StatefulWidget { class NewGroupChatOverviewScreen extends StatefulWidget {
const NewGroupChatOverviewScreen({ const NewGroupChatOverviewScreen({
required this.options, required this.options,
required this.onPressCompleteGroupChatCreation, required this.onPressCompleteGroupChatCreation,
required this.service, required this.service,
required this.users,
this.translations = const ChatTranslations.empty(), this.translations = const ChatTranslations.empty(),
super.key, super.key,
}); });
@ -18,8 +20,12 @@ class NewGroupChatOverviewScreen extends StatefulWidget {
final ChatOptions options; final ChatOptions options;
final ChatTranslations translations; final ChatTranslations translations;
final ChatService service; final ChatService service;
final List<ChatUserModel> users; final Function(
final Function(List<ChatUserModel>, String) onPressCompleteGroupChatCreation; List<ChatUserModel> users,
String groupchatName,
String? groupchatBio,
Uint8List? imageBytes,
) onPressCompleteGroupChatCreation;
@override @override
State<NewGroupChatOverviewScreen> createState() => State<NewGroupChatOverviewScreen> createState() =>
@ -28,15 +34,24 @@ class NewGroupChatOverviewScreen extends StatefulWidget {
class _NewGroupChatOverviewScreenState class _NewGroupChatOverviewScreenState
extends State<NewGroupChatOverviewScreen> { extends State<NewGroupChatOverviewScreen> {
final TextEditingController _textEditingController = TextEditingController(); final TextEditingController _chatNameController = TextEditingController();
final TextEditingController _bioController = TextEditingController();
Uint8List? image;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
var formKey = GlobalKey<FormState>(); var formKey = GlobalKey<FormState>();
var isPressed = false; var isPressed = false;
var users = widget.service.chatOverviewService.currentlySelectedUsers;
void onUploadImage(groupImage) {
setState(() {
image = groupImage;
});
}
return Scaffold( return Scaffold(
backgroundColor: theme.colorScheme.surface,
appBar: AppBar( appBar: AppBar(
iconTheme: theme.appBarTheme.iconTheme ?? iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white), const IconThemeData(color: Colors.white),
@ -46,15 +61,113 @@ class _NewGroupChatOverviewScreenState
style: theme.appBarTheme.titleTextStyle, style: theme.appBarTheme.titleTextStyle,
), ),
), ),
body: Padding( body: Stack(
padding: const EdgeInsets.all(16.0), children: [
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Form( child: Form(
key: formKey, key: formKey,
child: TextFormField( child: Column(
controller: _textEditingController, crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 40,
),
Center(
child: Stack(
children: [
GestureDetector(
onTap: () async {
await onPressSelectImage(
context,
widget.translations,
widget.options,
onUploadImage,
);
},
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFD9D9D9),
borderRadius: BorderRadius.circular(40),
image: image != null
? DecorationImage(
image: MemoryImage(image!),
fit: BoxFit.cover,
)
: null,
),
child: image == null
? const Icon(Icons.image)
: null,
),
),
if (image != null)
Positioned.directional(
textDirection: Directionality.of(context),
end: 0,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: const Color(0xFFBCBCBC),
borderRadius: BorderRadius.circular(40),
),
child: Center(
child: GestureDetector(
onTap: () {
setState(() {
image = null;
});
},
child: const Icon(
Icons.close,
size: 12,
),
),
),
),
)
else
const SizedBox.shrink(),
],
),
),
const SizedBox(
height: 40,
),
Text(
widget.translations.groupChatNameFieldHeader,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 12,
),
TextFormField(
style: theme.textTheme.bodySmall,
controller: _chatNameController,
decoration: InputDecoration( decoration: InputDecoration(
fillColor: Colors.white,
filled: true,
hintText: widget.translations.groupNameHintText, hintText: widget.translations.groupNameHintText,
hintStyle: theme.inputDecorationTheme.hintStyle, hintStyle: theme.textTheme.bodyMedium!.copyWith(
color: theme.textTheme.bodyMedium!.color!
.withOpacity(0.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
),
), ),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
@ -65,27 +178,143 @@ class _NewGroupChatOverviewScreenState
return null; return null;
}, },
), ),
const SizedBox(
height: 16,
),
Text(
widget.translations.groupBioFieldHeader,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 12,
),
TextFormField(
style: theme.textTheme.bodySmall,
controller: _bioController,
minLines: null,
maxLines: 5,
decoration: InputDecoration(
fillColor: Colors.white,
filled: true,
hintText: widget.translations.groupBioHintText,
hintStyle: theme.textTheme.bodyMedium!.copyWith(
color: theme.textTheme.bodyMedium!.color!
.withOpacity(0.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
), ),
), ),
floatingActionButton: FloatingActionButton( focusedBorder: OutlineInputBorder(
backgroundColor: Theme.of(context).primaryColor, borderRadius: BorderRadius.circular(12),
onPressed: () async { borderSide: const BorderSide(
color: Colors.transparent,
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return widget.translations.groupBioValidatorEmpty;
}
return null;
},
),
const SizedBox(
height: 16,
),
Text(
"${widget.translations.selectedMembersHeader}"
"${users.length}",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 12,
),
Wrap(
children: [
...users.map(
_selectedUser,
),
],
),
const SizedBox(
height: 80,
),
],
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 80,
),
child: FilledButton(
onPressed: users.isNotEmpty
? () async {
if (!isPressed) { if (!isPressed) {
isPressed = true; isPressed = true;
if (formKey.currentState!.validate()) { if (formKey.currentState!.validate()) {
await widget.onPressCompleteGroupChatCreation( await widget.onPressCompleteGroupChatCreation(
widget.users, users,
_textEditingController.text, _chatNameController.text,
_bioController.text,
image,
); );
} }
isPressed = false; isPressed = false;
} }
}, }
child: const Icon( : null,
Icons.check_circle, child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.translations.createGroupChatButton,
style: theme.textTheme.displayLarge,
),
],
), ),
), ),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ),
),
],
),
// floatingActionButton: FloatingActionButton(
); );
} }
Widget _selectedUser(ChatUserModel user) => GestureDetector(
onTap: () {
setState(() {
widget.service.chatOverviewService
.removeCurrentlySelectedUser(user);
});
},
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: widget.options.userAvatarBuilder(
user,
40,
),
),
Positioned.directional(
textDirection: Directionality.of(context),
end: 0,
child: const Icon(
Icons.cancel,
size: 20,
),
),
],
),
);
} }

View file

@ -23,7 +23,6 @@ class NewGroupChatScreen extends StatefulWidget {
class _NewGroupChatScreenState extends State<NewGroupChatScreen> { class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
final FocusNode _textFieldFocusNode = FocusNode(); final FocusNode _textFieldFocusNode = FocusNode();
List<ChatUserModel> selectedUserList = [];
bool _isSearching = false; bool _isSearching = false;
String query = ""; String query = "";
@ -51,19 +50,19 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
} 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 Stack(
children: [
_buildUserList(snapshot.data!),
NextButton(
service: widget.service,
onPressGroupChatOverview: widget.onPressGroupChatOverview,
),
],
);
} }
return const SizedBox.shrink(); return const SizedBox.shrink();
}, },
), ),
floatingActionButton: FloatingActionButton(
backgroundColor: Theme.of(context).primaryColor,
onPressed: () async {
await widget.onPressGroupChatOverview(selectedUserList);
},
child: const Icon(Icons.arrow_forward_ios),
),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
); );
} }
@ -80,14 +79,15 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
}, },
decoration: InputDecoration( decoration: InputDecoration(
hintText: widget.translations.searchPlaceholder, hintText: widget.translations.searchPlaceholder,
hintStyle: theme.inputDecorationTheme.hintStyle, hintStyle:
theme.textTheme.bodyMedium!.copyWith(color: Colors.white),
focusedBorder: UnderlineInputBorder( focusedBorder: UnderlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
), ),
), ),
style: theme.inputDecorationTheme.hintStyle, style: theme.textTheme.bodySmall!.copyWith(color: Colors.white),
cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white, cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white,
) )
: Text( : Text(
@ -140,9 +140,74 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
return UserList( return UserList(
filteredUsers: filteredUsers, filteredUsers: filteredUsers,
selectedUserList: selectedUserList,
options: widget.options, options: widget.options,
translations: widget.translations, translations: widget.translations,
service: widget.service,
);
}
}
class NextButton extends StatefulWidget {
const NextButton({
required this.service,
required this.onPressGroupChatOverview,
super.key,
});
final ChatService service;
final Function(List<ChatUserModel>) onPressGroupChatOverview;
@override
State<NextButton> createState() => _NextButtonState();
}
class _NextButtonState extends State<NextButton> {
@override
void initState() {
widget.service.chatOverviewService.addListener(_listen);
super.initState();
}
@override
void dispose() {
widget.service.chatOverviewService.removeListener(_listen);
super.dispose();
}
void _listen() {
setState(() {});
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 80,
),
child: FilledButton(
onPressed: widget
.service.chatOverviewService.currentlySelectedUsers.isNotEmpty
? () async {
await widget.onPressGroupChatOverview(
widget.service.chatOverviewService.currentlySelectedUsers,
);
}
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Next",
style: theme.textTheme.displayLarge,
),
],
),
),
),
); );
} }
} }
@ -150,16 +215,16 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
class UserList extends StatefulWidget { class UserList extends StatefulWidget {
const UserList({ const UserList({
required this.filteredUsers, required this.filteredUsers,
required this.selectedUserList,
required this.options, required this.options,
required this.translations, required this.translations,
required this.service,
super.key, super.key,
}); });
final List<ChatUserModel> filteredUsers; final List<ChatUserModel> filteredUsers;
final List<ChatUserModel> selectedUserList;
final ChatOptions options; final ChatOptions options;
final ChatTranslations translations; final ChatTranslations translations;
final ChatService service;
@override @override
State<UserList> createState() => _UserListState(); State<UserList> createState() => _UserListState();
@ -167,75 +232,70 @@ class UserList extends StatefulWidget {
class _UserListState extends State<UserList> { class _UserListState extends State<UserList> {
@override @override
Widget build(BuildContext context) => ListView.builder( void initState() {
widget.service.chatOverviewService.addListener(_listen);
super.initState();
}
@override
void dispose() {
widget.service.chatOverviewService.removeListener(_listen);
super.dispose();
}
void _listen() {
setState(() {});
}
@override
Widget build(BuildContext context) => Padding(
padding: widget.options.paddingAroundChatList ??
const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
child: ListView.builder(
itemCount: widget.filteredUsers.length, itemCount: widget.filteredUsers.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
var user = widget.filteredUsers[index]; var user = widget.filteredUsers[index];
var isSelected = widget.selectedUserList var isSelected = widget
.service.chatOverviewService.currentlySelectedUsers
.any((selectedUser) => selectedUser == user); .any((selectedUser) => selectedUser == user);
var theme = Theme.of(context); var theme = Theme.of(context);
return DecoratedBox( return widget.options.chatRowContainerBuilder(
decoration: BoxDecoration( Row(
border: Border( mainAxisAlignment: MainAxisAlignment.spaceBetween,
bottom: BorderSide( children: [
color: theme.colorScheme.secondary.withOpacity(0.3), Row(
width: 0.5, children: [
widget.options.userAvatarBuilder(user, 44),
const SizedBox(
width: 12,
), ),
Text(
user.fullName ?? widget.translations.anonymousUser,
style: theme.textTheme.titleMedium,
), ),
],
), ),
child: InkWell( Checkbox(
onTap: () { value: isSelected,
onChanged: (value) {
setState(() { setState(() {
if (widget.selectedUserList.contains(user)) { if (widget
widget.selectedUserList.remove(user); .service.chatOverviewService.currentlySelectedUsers
.contains(user)) {
widget.service.chatOverviewService
.removeCurrentlySelectedUser(user);
} else { } else {
widget.selectedUserList.add(user); widget.service.chatOverviewService
.addCurrentlySelectedUser(user);
} }
}); });
}, },
child: Padding(
padding: widget.options.paddingAroundChatList ??
const EdgeInsets.fromLTRB(28, 8, 28, 8),
child: ColoredBox(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 30,
),
child: Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: widget.options.userAvatarBuilder(user, 40.0),
),
Expanded(
child: Container(
height: 40,
alignment: Alignment.centerLeft,
child: Text(
user.fullName ??
widget.translations.anonymousUser,
style: theme.textTheme.bodyLarge,
),
),
),
if (isSelected) ...[
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Icon(
Icons.check_circle,
color: theme.colorScheme.primary,
),
), ),
], ],
],
),
),
),
),
), ),
context,
); );
}, },
),
); );
} }

View file

@ -23,11 +23,39 @@ class DateFormatter {
.inDays == .inDays ==
0; 0;
bool _isYesterday(DateTime date) =>
DateTime(
date.year,
date.month,
date.day,
)
.difference(
DateTime(
_now.year,
_now.month,
_now.day,
),
)
.inDays ==
-1;
bool _isThisYear(DateTime date) => date.year == _now.year;
String format({ String format({
required DateTime date, required DateTime date,
bool showFullDate = false, bool showFullDate = false,
}) => }) {
DateFormat( if(showFullDate) {
_isToday(date) ? "HH:mm" : 'dd-MM-yyyy${showFullDate ? ' HH:mm' : ''}', return DateFormat("dd - MM - yyyy HH:mm").format(date);
).format(date); }
if (_isToday(date)) {
return DateFormat("HH:mm").format(date);
} else if (_isYesterday(date)) {
return "yesterday";
} else if (_isThisYear(date)) {
return DateFormat("dd MMMM").format(date);
} else {
return DateFormat("dd - MM - yyyy").format(date);
}
}
} }

View file

@ -4,7 +4,7 @@
name: flutter_chat_view name: flutter_chat_view
description: A standard flutter package. description: A standard flutter package.
version: 3.0.1 version: 3.1.0
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
@ -25,7 +25,7 @@ dependencies:
version: ^1.0.5 version: ^1.0.5
flutter_profile: flutter_profile:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^1.3.0 version: ^1.5.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: