mirror of
https://github.com/Iconica-Development/flutter_chat.git
synced 2025-05-18 18:33:49 +02:00
feat: ui update
This commit is contained in:
parent
644615f026
commit
8f13d87a23
28 changed files with 1195 additions and 611 deletions
|
@ -1,5 +1,8 @@
|
|||
## 3.1.0
|
||||
- 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
|
||||
|
|
|
@ -16,15 +16,6 @@ dependencies:
|
|||
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:
|
||||
flutter_test:
|
||||
|
|
|
@ -104,6 +104,10 @@ Widget _chatDetailScreenRoute(
|
|||
if (configuration.onPressUserProfile != null) {
|
||||
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(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _chatProfileScreenRoute(
|
||||
|
@ -111,6 +115,7 @@ Widget _chatDetailScreenRoute(
|
|||
context,
|
||||
chatId,
|
||||
user.id,
|
||||
currentUserId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -142,7 +147,10 @@ Widget _chatDetailScreenRoute(
|
|||
if (configuration.onPressChatTitle?.call(context, chat) != null) {
|
||||
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(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _chatProfileScreenRoute(
|
||||
|
@ -150,6 +158,7 @@ Widget _chatDetailScreenRoute(
|
|||
context,
|
||||
chatId,
|
||||
null,
|
||||
currentUserId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -168,17 +177,23 @@ Widget _chatProfileScreenRoute(
|
|||
BuildContext context,
|
||||
String chatId,
|
||||
String? userId,
|
||||
String currentUserId,
|
||||
) =>
|
||||
ChatProfileScreen(
|
||||
options: configuration.chatOptionsBuilder(context),
|
||||
translations: configuration.translations,
|
||||
chatService: configuration.chatService,
|
||||
chatId: chatId,
|
||||
userId: userId,
|
||||
currentUserId: currentUserId,
|
||||
onTapUser: (user) async {
|
||||
if (configuration.onPressUserProfile != null) {
|
||||
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(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _chatProfileScreenRoute(
|
||||
|
@ -186,10 +201,40 @@ Widget _chatProfileScreenRoute(
|
|||
context,
|
||||
chatId,
|
||||
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.
|
||||
|
@ -207,6 +252,8 @@ Widget _newChatScreenRoute(
|
|||
showGroupChatButton: configuration.enableGroupChatCreation,
|
||||
onPressCreateGroupChat: () async {
|
||||
configuration.onPressCreateGroupChat?.call();
|
||||
configuration.chatService.chatOverviewService
|
||||
.clearCurrentlySelectedUsers();
|
||||
if (context.mounted) {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
|
@ -223,13 +270,13 @@ Widget _newChatScreenRoute(
|
|||
if (configuration.onPressCreateChat != null) return;
|
||||
var chat = await configuration.chatService.chatOverviewService
|
||||
.getChatByUser(user);
|
||||
debugPrint("Chat is ${chat.id}");
|
||||
if (chat.id == null) {
|
||||
chat = await configuration.chatService.chatOverviewService
|
||||
.storeChatIfNot(
|
||||
PersonalChatModel(
|
||||
user: user,
|
||||
),
|
||||
null,
|
||||
);
|
||||
}
|
||||
if (context.mounted) {
|
||||
|
@ -277,19 +324,20 @@ Widget _newGroupChatOverviewScreenRoute(
|
|||
options: configuration.chatOptionsBuilder(context),
|
||||
translations: configuration.translations,
|
||||
service: configuration.chatService,
|
||||
users: users,
|
||||
onPressCompleteGroupChatCreation: (users, groupChatName) async {
|
||||
onPressCompleteGroupChatCreation:
|
||||
(users, groupChatName, groupBio, image) async {
|
||||
configuration.onPressCompleteGroupChatCreation
|
||||
?.call(users, groupChatName);
|
||||
?.call(users, groupChatName, image);
|
||||
if (configuration.onPressCreateGroupChat != null) return;
|
||||
var chat =
|
||||
await configuration.chatService.chatOverviewService.storeChatIfNot(
|
||||
GroupChatModel(
|
||||
canBeDeleted: true,
|
||||
title: groupChatName,
|
||||
imageUrl: "https://picsum.photos/200/300",
|
||||
users: users,
|
||||
bio: groupBio,
|
||||
),
|
||||
image,
|
||||
);
|
||||
if (context.mounted) {
|
||||
await Navigator.of(context).pushReplacement(
|
||||
|
|
|
@ -152,6 +152,7 @@ List<GoRoute> getChatStoryRoutes(
|
|||
PersonalChatModel(
|
||||
user: user,
|
||||
),
|
||||
null,
|
||||
);
|
||||
}
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
return buildScreenWithoutTransition(
|
||||
context: context,
|
||||
|
@ -211,25 +216,25 @@ List<GoRoute> getChatStoryRoutes(
|
|||
pageBuilder: (context, state) {
|
||||
var service = configuration.chatServiceBuilder?.call(context) ??
|
||||
configuration.chatService;
|
||||
var users = state.extra! as List<ChatUserModel>;
|
||||
|
||||
var newGroupChatOverviewScreen = NewGroupChatOverviewScreen(
|
||||
options: configuration.chatOptionsBuilder(context),
|
||||
translations: configuration.translationsBuilder?.call(context) ??
|
||||
configuration.translations,
|
||||
service: service,
|
||||
users: users,
|
||||
onPressCompleteGroupChatCreation: (users, groupChatName) async {
|
||||
onPressCompleteGroupChatCreation:
|
||||
(users, groupChatName, groupBio, image) async {
|
||||
configuration.onPressCompleteGroupChatCreation
|
||||
?.call(users, groupChatName);
|
||||
?.call(users, groupChatName, image);
|
||||
var chat = await configuration.chatService.chatOverviewService
|
||||
.storeChatIfNot(
|
||||
GroupChatModel(
|
||||
canBeDeleted: true,
|
||||
title: groupChatName,
|
||||
imageUrl: "https://picsum.photos/200/300",
|
||||
users: users,
|
||||
bio: groupBio,
|
||||
),
|
||||
image,
|
||||
);
|
||||
if (context.mounted) {
|
||||
context.go(
|
||||
|
@ -259,13 +264,21 @@ List<GoRoute> getChatStoryRoutes(
|
|||
var id = userId == "null" ? null : userId;
|
||||
var service = configuration.chatServiceBuilder?.call(context) ??
|
||||
configuration.chatService;
|
||||
ChatUserModel? currentUser;
|
||||
String? currentUserId;
|
||||
Future.delayed(Duration.zero, () async {
|
||||
currentUser = await service.chatUserService.getCurrentUser();
|
||||
currentUserId = currentUser!.id;
|
||||
});
|
||||
|
||||
var profileScreen = ChatProfileScreen(
|
||||
options: configuration.chatOptionsBuilder(context),
|
||||
translations: configuration.translationsBuilder?.call(context) ??
|
||||
configuration.translations,
|
||||
chatService: service,
|
||||
chatId: chatId!,
|
||||
userId: id,
|
||||
currentUserId: currentUserId!,
|
||||
onTapUser: (user) async {
|
||||
if (configuration.onPressUserProfile != null) {
|
||||
return configuration.onPressUserProfile!.call(context, user);
|
||||
|
@ -275,6 +288,26 @@ List<GoRoute> getChatStoryRoutes(
|
|||
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(
|
||||
context: context,
|
||||
|
|
|
@ -83,7 +83,11 @@ class ChatUserStoryConfiguration {
|
|||
final Function(ChatUserModel)? onPressCreateChat;
|
||||
|
||||
/// 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;
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ class FirebaseChatDocument {
|
|||
this.title,
|
||||
this.imageUrl,
|
||||
this.lastMessage,
|
||||
this.bio,
|
||||
});
|
||||
|
||||
/// Constructs a FirebaseChatDocument from JSON.
|
||||
|
@ -34,7 +35,8 @@ class FirebaseChatDocument {
|
|||
: FirebaseMessageDocument.fromJson(
|
||||
json["last_message"],
|
||||
null,
|
||||
);
|
||||
),
|
||||
bio = json["bio"];
|
||||
|
||||
/// The unique identifier of the chat document.
|
||||
final String? id;
|
||||
|
@ -60,6 +62,8 @@ class FirebaseChatDocument {
|
|||
/// The last message in the chat.
|
||||
final FirebaseMessageDocument? lastMessage;
|
||||
|
||||
final String? bio;
|
||||
|
||||
/// Converts the FirebaseChatDocument to JSON format.
|
||||
Map<String, dynamic> toJson() => {
|
||||
"title": title,
|
||||
|
@ -68,5 +72,6 @@ class FirebaseChatDocument {
|
|||
"last_used": lastUsed,
|
||||
"can_be_deleted": canBeDeleted,
|
||||
"users": users,
|
||||
"bio": bio,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -241,7 +241,6 @@ class FirebaseChatDetailService
|
|||
_cumulativeMessages = [];
|
||||
lastChat = chatId;
|
||||
lastMessage = null;
|
||||
debugPrint("Canceling messages stream");
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -4,16 +4,20 @@
|
|||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import "dart:async";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:cloud_firestore/cloud_firestore.dart";
|
||||
import "package:firebase_core/firebase_core.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/dto/firebase_chat_document.dart";
|
||||
import "package:flutter_chat_interface/flutter_chat_interface.dart";
|
||||
|
||||
/// Service class for managing chat overviews using Firebase.
|
||||
class FirebaseChatOverviewService implements ChatOverviewService {
|
||||
class FirebaseChatOverviewService
|
||||
with ChangeNotifier
|
||||
implements ChatOverviewService {
|
||||
late FirebaseFirestore _db;
|
||||
late FirebaseStorage _storage;
|
||||
late ChatUserService _userService;
|
||||
|
@ -38,6 +42,8 @@ class FirebaseChatOverviewService implements ChatOverviewService {
|
|||
_options = options ?? const FirebaseChatOptions();
|
||||
}
|
||||
|
||||
final List<ChatUserModel> _currentlySelectedUsers = [];
|
||||
|
||||
Future<int?> _addUnreadChatSubscription(
|
||||
String chatId,
|
||||
String userId,
|
||||
|
@ -285,6 +291,7 @@ class FirebaseChatOverviewService implements ChatOverviewService {
|
|||
imageUrl: chat?.imageUrl ?? "",
|
||||
users: users,
|
||||
canBeDeleted: chat?.canBeDeleted ?? true,
|
||||
bio: chat?.bio,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -338,7 +345,7 @@ class FirebaseChatOverviewService implements ChatOverviewService {
|
|||
///
|
||||
/// [chat]: The chat to be stored.
|
||||
@override
|
||||
Future<ChatModel> storeChatIfNot(ChatModel chat) async {
|
||||
Future<ChatModel> storeChatIfNot(ChatModel chat, Uint8List? image) async {
|
||||
if (chat.id == null) {
|
||||
var currentUser = await _userService.getCurrentUser();
|
||||
if (chat is PersonalChatModel) {
|
||||
|
@ -398,22 +405,41 @@ class FirebaseChatOverviewService implements ChatOverviewService {
|
|||
FirebaseChatDocument(
|
||||
personal: false,
|
||||
title: chat.title,
|
||||
imageUrl: chat.imageUrl,
|
||||
canBeDeleted: chat.canBeDeleted,
|
||||
users: userIds,
|
||||
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) {
|
||||
await _db
|
||||
.collection(_options.usersCollectionName)
|
||||
.doc(userId)
|
||||
.collection(_options.groupChatsCollectionName)
|
||||
.doc(reference.id)
|
||||
.doc(currentChat.id)
|
||||
.set({"users": userIds}, SetOptions(merge: true));
|
||||
}
|
||||
chat.id = reference.id;
|
||||
currentlySelectedUsers.clear();
|
||||
} else {
|
||||
throw Exception("Chat type not supported for firebase");
|
||||
}
|
||||
|
@ -478,4 +504,34 @@ class FirebaseChatOverviewService implements ChatOverviewService {
|
|||
.doc(chat.id)
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
name: flutter_chat_firebase
|
||||
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
|
||||
|
||||
|
|
|
@ -16,8 +16,9 @@ abstract class GroupChatModelInterface extends ChatModel {
|
|||
});
|
||||
|
||||
String get title;
|
||||
String get imageUrl;
|
||||
String? get imageUrl;
|
||||
List<ChatUserModel> get users;
|
||||
String? get bio;
|
||||
|
||||
@override
|
||||
GroupChatModelInterface copyWith({
|
||||
|
@ -30,6 +31,7 @@ abstract class GroupChatModelInterface extends ChatModel {
|
|||
String? imageUrl,
|
||||
List<ChatUserModel>? users,
|
||||
bool? canBeDeleted,
|
||||
String? bio,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -56,13 +58,14 @@ class GroupChatModel implements GroupChatModelInterface {
|
|||
GroupChatModel({
|
||||
required this.canBeDeleted,
|
||||
required this.title,
|
||||
required this.imageUrl,
|
||||
required this.users,
|
||||
this.imageUrl,
|
||||
this.id,
|
||||
this.messages,
|
||||
this.unreadMessages,
|
||||
this.lastUsed,
|
||||
this.lastMessage,
|
||||
this.bio,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -80,9 +83,11 @@ class GroupChatModel implements GroupChatModelInterface {
|
|||
@override
|
||||
final String title;
|
||||
@override
|
||||
final String imageUrl;
|
||||
final String? imageUrl;
|
||||
@override
|
||||
final List<ChatUserModel> users;
|
||||
@override
|
||||
final String? bio;
|
||||
|
||||
@override
|
||||
GroupChatModel copyWith({
|
||||
|
@ -95,6 +100,7 @@ class GroupChatModel implements GroupChatModelInterface {
|
|||
String? title,
|
||||
String? imageUrl,
|
||||
List<ChatUserModel>? users,
|
||||
String? bio,
|
||||
}) =>
|
||||
GroupChatModel(
|
||||
id: id ?? this.id,
|
||||
|
@ -106,5 +112,6 @@ class GroupChatModel implements GroupChatModelInterface {
|
|||
title: title ?? this.title,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
users: users ?? this.users,
|
||||
bio: bio ?? this.bio,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import "dart:typed_data";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_chat_interface/flutter_chat_interface.dart";
|
||||
|
||||
abstract class ChatOverviewService {
|
||||
abstract class ChatOverviewService extends ChangeNotifier {
|
||||
/// Retrieves a stream of chats.
|
||||
/// This stream is updated whenever a new chat is created.
|
||||
Stream<List<ChatModel>> getChatsStream();
|
||||
|
@ -18,8 +21,22 @@ abstract class ChatOverviewService {
|
|||
Future<void> readChat(ChatModel chat);
|
||||
|
||||
/// 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.
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
name: flutter_chat_interface
|
||||
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
|
||||
|
||||
environment:
|
||||
|
|
|
@ -7,6 +7,9 @@ import "package:flutter_chat_interface/flutter_chat_interface.dart";
|
|||
class LocalChatOverviewService
|
||||
with ChangeNotifier
|
||||
implements ChatOverviewService {
|
||||
/// The list of currently selected users.
|
||||
final List<ChatUserModel> _currentlySelectedUsers = [];
|
||||
|
||||
/// The list of personal chat models.
|
||||
final List<ChatModel> _chats = [];
|
||||
|
||||
|
@ -22,7 +25,6 @@ class LocalChatOverviewService
|
|||
_chats[index] = chat;
|
||||
_chatsController.addStream(Stream.value(_chats));
|
||||
notifyListeners();
|
||||
debugPrint("Chat updated: $chat");
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
|
@ -31,15 +33,12 @@ class LocalChatOverviewService
|
|||
_chats.removeWhere((element) => element.id == chat.id);
|
||||
_chatsController.add(_chats);
|
||||
notifyListeners();
|
||||
debugPrint("Chat deleted: $chat");
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ChatModel> getChatById(String 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);
|
||||
}
|
||||
|
||||
|
@ -59,7 +58,6 @@ class LocalChatOverviewService
|
|||
);
|
||||
chat.id = chat.hashCode.toString();
|
||||
_chats.add(chat);
|
||||
debugPrint("New chat created: $chat");
|
||||
}
|
||||
|
||||
_chatsController.add([..._chats]);
|
||||
|
@ -77,19 +75,42 @@ class LocalChatOverviewService
|
|||
Future<void> readChat(ChatModel chat) async => Future.value();
|
||||
|
||||
@override
|
||||
Future<ChatModel> storeChatIfNot(ChatModel chat) {
|
||||
Future<ChatModel> storeChatIfNot(ChatModel chat, Uint8List? image) {
|
||||
var chatExists = _chats.any((element) => element.id == chat.id);
|
||||
|
||||
if (!chatExists) {
|
||||
chat.id = chat.hashCode.toString();
|
||||
_chats.add(chat);
|
||||
_chatsController.add([..._chats]);
|
||||
currentlySelectedUsers.clear();
|
||||
notifyListeners();
|
||||
debugPrint("Chat stored: $chat");
|
||||
} else {
|
||||
debugPrint("Chat already exists: $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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ class LocalChatUserService implements ChatUserService {
|
|||
|
||||
@override
|
||||
Future<ChatUserModel?> getCurrentUser() =>
|
||||
Future.value(const ChatUserModel());
|
||||
Future.value(users.where((element) => element.id == "3").first);
|
||||
|
||||
@override
|
||||
Future<ChatUserModel?> getUser(String id) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: flutter_chat_local
|
||||
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
|
||||
|
||||
|
|
|
@ -103,11 +103,7 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
|
|||
Text(
|
||||
widget.message.sender.fullName ??
|
||||
widget.translations.anonymousUser,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: theme.textTheme.labelMedium?.color,
|
||||
),
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
|
@ -115,13 +111,9 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
|
|||
child: Text(
|
||||
_dateFormatter.format(
|
||||
date: widget.message.timestamp,
|
||||
showFullDate: true,
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w300,
|
||||
color: Color(0xFFBBBBBB),
|
||||
showFullDate: false,
|
||||
),
|
||||
style: theme.textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -137,10 +129,7 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
|
|||
Flexible(
|
||||
child: Text(
|
||||
(widget.message as ChatTextMessageModel).text,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: theme.textTheme.labelMedium?.color,
|
||||
),
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
if (widget.showTime &&
|
||||
|
@ -155,7 +144,7 @@ class _ChatDetailRowState extends State<ChatDetailRow> {
|
|||
)
|
||||
.split(" ")
|
||||
.last,
|
||||
style: theme.textTheme.bodySmall,
|
||||
style: theme.textTheme.labelSmall,
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
],
|
||||
|
|
|
@ -49,9 +49,7 @@ class ChatRow extends StatelessWidget {
|
|||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: unreadMessages > 0
|
||||
? theme.textTheme.bodyLarge
|
||||
: theme.textTheme.bodyMedium,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
if (subTitle != null) ...[
|
||||
Padding(
|
||||
|
@ -59,8 +57,10 @@ class ChatRow extends StatelessWidget {
|
|||
child: Text(
|
||||
subTitle!,
|
||||
style: unreadMessages > 0
|
||||
? theme.textTheme.bodyLarge
|
||||
: theme.textTheme.bodyMedium,
|
||||
? theme.textTheme.bodySmall!.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
)
|
||||
: theme.textTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
),
|
||||
|
@ -79,10 +79,7 @@ class ChatRow extends StatelessWidget {
|
|||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: Text(
|
||||
lastUsed!,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFBBBBBB),
|
||||
fontSize: 14,
|
||||
),
|
||||
style: theme.textTheme.labelSmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
},
|
||||
);
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
import "package:flutter/material.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_profile/flutter_profile.dart";
|
||||
|
||||
|
@ -57,15 +56,16 @@ Widget _createNewChatButton(
|
|||
BuildContext context,
|
||||
VoidCallback onPressed,
|
||||
ChatTranslations translations,
|
||||
) =>
|
||||
Padding(
|
||||
) {
|
||||
var theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 24,
|
||||
horizontal: 5,
|
||||
horizontal: 4,
|
||||
),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
fixedSize: const Size(254, 44),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(56),
|
||||
|
@ -74,14 +74,11 @@ Widget _createNewChatButton(
|
|||
onPressed: onPressed,
|
||||
child: Text(
|
||||
translations.newChatButton,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
),
|
||||
style: theme.textTheme.displayLarge,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createMessageInput(
|
||||
TextEditingController textEditingController,
|
||||
|
@ -91,6 +88,7 @@ Widget _createMessageInput(
|
|||
) {
|
||||
var theme = Theme.of(context);
|
||||
return TextField(
|
||||
style: theme.textTheme.bodySmall,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
controller: textEditingController,
|
||||
decoration: InputDecoration(
|
||||
|
@ -111,7 +109,9 @@ Widget _createMessageInput(
|
|||
horizontal: 30,
|
||||
),
|
||||
hintText: translations.messagePlaceholder,
|
||||
hintStyle: theme.inputDecorationTheme.hintStyle,
|
||||
hintStyle: theme.textTheme.bodyMedium!.copyWith(
|
||||
color: theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
|
||||
),
|
||||
fillColor: Colors.white,
|
||||
filled: true,
|
||||
border: const OutlineInputBorder(
|
||||
|
@ -127,24 +127,33 @@ Widget _createMessageInput(
|
|||
|
||||
Widget _createChatRowContainer(
|
||||
Widget chatRow,
|
||||
) =>
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12.0,
|
||||
horizontal: 10.0,
|
||||
),
|
||||
child: ColoredBox(
|
||||
BuildContext context,
|
||||
) {
|
||||
var theme = Theme.of(context);
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.dividerColor,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: chatRow,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createImagePickerContainer(
|
||||
VoidCallback onClose,
|
||||
ChatTranslations translations,
|
||||
BuildContext context,
|
||||
) =>
|
||||
Container(
|
||||
) {
|
||||
var theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
color: Colors.white,
|
||||
child: ImagePicker(
|
||||
|
@ -155,19 +164,23 @@ Widget _createImagePickerContainer(
|
|||
iconSize: 60.0,
|
||||
makePhotoText: translations.takePicture,
|
||||
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,
|
||||
child: Text(
|
||||
translations.cancelImagePickerBtn,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
style: theme.textTheme.bodyMedium!.copyWith(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Scaffold _createScaffold(
|
||||
AppBar appbar,
|
||||
|
@ -185,20 +198,27 @@ Widget _createUserAvatar(
|
|||
double size,
|
||||
) =>
|
||||
Avatar(
|
||||
boxfit: BoxFit.cover,
|
||||
user: User(
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
imageUrl: user.imageUrl,
|
||||
imageUrl: user.imageUrl != "" ? user.imageUrl : null,
|
||||
),
|
||||
size: size,
|
||||
);
|
||||
|
||||
Widget _createGroupAvatar(
|
||||
String groupName,
|
||||
String imageUrl,
|
||||
String? imageUrl,
|
||||
double size,
|
||||
) =>
|
||||
ChatImage(
|
||||
image: imageUrl,
|
||||
Avatar(
|
||||
boxfit: BoxFit.cover,
|
||||
user: User(
|
||||
firstName: groupName,
|
||||
lastName: null,
|
||||
imageUrl: imageUrl != "" ? imageUrl : null,
|
||||
),
|
||||
size: size,
|
||||
);
|
||||
|
||||
|
@ -249,6 +269,7 @@ typedef TextInputBuilder = Widget Function(
|
|||
|
||||
typedef ContainerBuilder = Widget Function(
|
||||
Widget child,
|
||||
BuildContext context,
|
||||
);
|
||||
|
||||
typedef ImagePickerContainerBuilder = Widget Function(
|
||||
|
@ -270,7 +291,7 @@ typedef UserAvatarBuilder = Widget Function(
|
|||
|
||||
typedef GroupAvatarBuilder = Widget Function(
|
||||
String groupName,
|
||||
String imageUrl,
|
||||
String? imageUrl,
|
||||
double size,
|
||||
);
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ class ChatTranslations {
|
|||
required this.newChatButton,
|
||||
required this.newGroupChatButton,
|
||||
required this.newChatTitle,
|
||||
required this.deleteChatButton,
|
||||
required this.image,
|
||||
required this.searchPlaceholder,
|
||||
required this.startTyping,
|
||||
|
@ -40,6 +39,13 @@ class ChatTranslations {
|
|||
required this.groupNameValidatorTooLong,
|
||||
required this.groupNameHintText,
|
||||
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
|
||||
|
@ -47,7 +53,7 @@ class ChatTranslations {
|
|||
this.chatsTitle = "Chats",
|
||||
this.chatsUnread = "unread",
|
||||
this.newChatButton = "Start chat",
|
||||
this.newGroupChatButton = "Create a group chat",
|
||||
this.newGroupChatButton = "Create a groupchat",
|
||||
this.newChatTitle = "Start a chat",
|
||||
this.image = "Image",
|
||||
this.searchPlaceholder = "Search...",
|
||||
|
@ -58,25 +64,31 @@ class ChatTranslations {
|
|||
this.writeFirstMessageInGroupChat =
|
||||
"Write the first message in this group chat",
|
||||
this.imageUploading = "Image is uploading...",
|
||||
this.deleteChatButton = "Delete",
|
||||
this.deleteChatModalTitle = "Delete chat",
|
||||
this.deleteChatModalDescription =
|
||||
"Are you sure you want to delete this chat?",
|
||||
this.deleteChatModalCancel = "Cancel",
|
||||
this.deleteChatModalConfirm = "Delete",
|
||||
this.deleteChatModalConfirm = "Confirm",
|
||||
this.noUsersFound = "No users were found to start a chat with",
|
||||
this.noChatsFound = "Click on 'Start a chat' to create a new chat",
|
||||
this.anonymousUser = "Anonymous user",
|
||||
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.uploadFile = "UPLOAD FILE",
|
||||
this.takePicture = "TAKE PICTURE",
|
||||
this.groupNameHintText = "Group chat name",
|
||||
this.groupNameHintText = "Groupchat name",
|
||||
this.groupNameValidatorEmpty = "Please enter a group chat name",
|
||||
this.groupNameValidatorTooLong =
|
||||
"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;
|
||||
|
@ -84,7 +96,6 @@ class ChatTranslations {
|
|||
final String newChatButton;
|
||||
final String newGroupChatButton;
|
||||
final String newChatTitle;
|
||||
final String deleteChatButton;
|
||||
final String image;
|
||||
final String searchPlaceholder;
|
||||
final String startTyping;
|
||||
|
@ -104,6 +115,10 @@ class ChatTranslations {
|
|||
final String imagePickerTitle;
|
||||
final String uploadFile;
|
||||
final String takePicture;
|
||||
final String groupChatNameFieldHeader;
|
||||
final String groupBioFieldHeader;
|
||||
final String selectedMembersHeader;
|
||||
final String createGroupChatButton;
|
||||
|
||||
/// Shown when the user has no name
|
||||
final String anonymousUser;
|
||||
|
@ -111,6 +126,9 @@ class ChatTranslations {
|
|||
final String groupNameValidatorTooLong;
|
||||
final String groupNameHintText;
|
||||
final String newGroupChatTitle;
|
||||
final String groupBioHintText;
|
||||
final String groupProfileBioHeader;
|
||||
final String groupBioValidatorEmpty;
|
||||
|
||||
// copyWith method to override the default values
|
||||
ChatTranslations copyWith({
|
||||
|
@ -119,7 +137,6 @@ class ChatTranslations {
|
|||
String? newChatButton,
|
||||
String? newGroupChatButton,
|
||||
String? newChatTitle,
|
||||
String? deleteChatButton,
|
||||
String? image,
|
||||
String? searchPlaceholder,
|
||||
String? startTyping,
|
||||
|
@ -144,6 +161,13 @@ class ChatTranslations {
|
|||
String? groupNameValidatorTooLong,
|
||||
String? groupNameHintText,
|
||||
String? newGroupChatTitle,
|
||||
String? groupBioHintText,
|
||||
String? groupProfileBioHeader,
|
||||
String? groupBioValidatorEmpty,
|
||||
String? groupChatNameFieldHeader,
|
||||
String? groupBioFieldHeader,
|
||||
String? selectedMembersHeader,
|
||||
String? createGroupChatButton,
|
||||
}) =>
|
||||
ChatTranslations(
|
||||
chatsTitle: chatsTitle ?? this.chatsTitle,
|
||||
|
@ -151,7 +175,6 @@ class ChatTranslations {
|
|||
newChatButton: newChatButton ?? this.newChatButton,
|
||||
newGroupChatButton: newGroupChatButton ?? this.newGroupChatButton,
|
||||
newChatTitle: newChatTitle ?? this.newChatTitle,
|
||||
deleteChatButton: deleteChatButton ?? this.deleteChatButton,
|
||||
image: image ?? this.image,
|
||||
searchPlaceholder: searchPlaceholder ?? this.searchPlaceholder,
|
||||
startTyping: startTyping ?? this.startTyping,
|
||||
|
@ -177,9 +200,23 @@ class ChatTranslations {
|
|||
uploadFile: uploadFile ?? this.uploadFile,
|
||||
takePicture: takePicture ?? this.takePicture,
|
||||
anonymousUser: anonymousUser ?? this.anonymousUser,
|
||||
groupNameValidatorEmpty: this.groupNameValidatorEmpty,
|
||||
groupNameValidatorTooLong: this.groupNameValidatorTooLong,
|
||||
groupNameHintText: this.groupNameHintText,
|
||||
newGroupChatTitle: this.newGroupChatTitle,
|
||||
groupNameValidatorEmpty:
|
||||
groupNameValidatorEmpty ?? this.groupNameValidatorEmpty,
|
||||
groupNameValidatorTooLong:
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import "package:flutter/material.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_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 {
|
||||
const ChatDetailScreen({
|
||||
|
@ -139,29 +139,6 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
|||
Widget build(BuildContext 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>(
|
||||
// ignore: discarded_futures
|
||||
future: widget.service.chatOverviewService.getChatById(widget.chatId),
|
||||
|
@ -175,7 +152,6 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
|||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.appBarTheme.backgroundColor,
|
||||
iconTheme: theme.appBarTheme.iconTheme ??
|
||||
const IconThemeData(color: Colors.white),
|
||||
centerTitle: true,
|
||||
|
@ -192,7 +168,7 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
|||
child: widget.chatTitleBuilder?.call(chatTitle) ??
|
||||
Text(
|
||||
chatTitle,
|
||||
style: theme.appBarTheme.titleTextStyle,
|
||||
style: theme.textTheme.headlineLarge,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
@ -253,7 +229,12 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
|||
ChatBottom(
|
||||
chat: chatModel,
|
||||
messageInputBuilder: widget.options.messageInputBuilder,
|
||||
onPressSelectImage: onPressSelectImage,
|
||||
onPressSelectImage: () async => onPressSelectImage.call(
|
||||
context,
|
||||
widget.translations,
|
||||
widget.options,
|
||||
widget.onUploadImage,
|
||||
),
|
||||
onMessageSubmit: widget.onMessageSubmit,
|
||||
translations: widget.translations,
|
||||
iconColor: widget.iconColor,
|
||||
|
|
|
@ -8,6 +8,9 @@ class ChatProfileScreen extends StatefulWidget {
|
|||
required this.chatId,
|
||||
required this.translations,
|
||||
required this.onTapUser,
|
||||
required this.options,
|
||||
required this.onPressStartChat,
|
||||
required this.currentUserId,
|
||||
this.userId,
|
||||
super.key,
|
||||
});
|
||||
|
@ -27,6 +30,15 @@ class ChatProfileScreen extends StatefulWidget {
|
|||
/// Callback function for tapping on a user.
|
||||
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
|
||||
State<ChatProfileScreen> createState() => _ProfileScreenState();
|
||||
}
|
||||
|
@ -65,9 +77,9 @@ class _ProfileScreenState extends State<ChatProfileScreen> {
|
|||
imageUrl: data.imageUrl,
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.appBarTheme.backgroundColor,
|
||||
iconTheme: theme.appBarTheme.iconTheme ??
|
||||
const IconThemeData(color: Colors.white),
|
||||
title: Text(
|
||||
|
@ -78,16 +90,27 @@ class _ProfileScreenState extends State<ChatProfileScreen> {
|
|||
: (data is GroupChatModel)
|
||||
? data.title
|
||||
: "",
|
||||
style: theme.appBarTheme.titleTextStyle,
|
||||
style: theme.textTheme.headlineLarge,
|
||||
),
|
||||
),
|
||||
body: snapshot.hasData
|
||||
? ListView(
|
||||
? Stack(
|
||||
children: [
|
||||
ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: Avatar(
|
||||
user: user,
|
||||
child: Column(
|
||||
children: [
|
||||
widget.options.userAvatarBuilder(
|
||||
ChatUserModel(
|
||||
firstName: user!.firstName,
|
||||
lastName: user.lastName,
|
||||
imageUrl: user.imageUrl,
|
||||
),
|
||||
60,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
|
@ -97,44 +120,93 @@ class _ProfileScreenState extends State<ChatProfileScreen> {
|
|||
if (data is GroupChatModel) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 100,
|
||||
vertical: 20,
|
||||
vertical: 24,
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Avatar(
|
||||
user: user,
|
||||
Text(
|
||||
widget.translations.groupProfileBioHeader,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
|
|
|
@ -77,10 +77,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
var theme = Theme.of(context);
|
||||
return widget.options.scaffoldBuilder(
|
||||
AppBar(
|
||||
backgroundColor: theme.appBarTheme.backgroundColor,
|
||||
title: Text(
|
||||
translations.chatsTitle,
|
||||
style: theme.appBarTheme.titleTextStyle,
|
||||
style: theme.textTheme.headlineLarge,
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
|
@ -96,9 +95,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
child: Text(
|
||||
"${snapshot.data ?? 0} ${translations.chatsUnread}",
|
||||
style: widget.unreadMessageTextStyle ??
|
||||
const TextStyle(
|
||||
theme.textTheme.bodySmall!.copyWith(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -147,119 +145,23 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.colorScheme.secondary
|
||||
.withOpacity(0.3),
|
||||
color: theme.dividerColor,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) => !(widget
|
||||
.disableDismissForPermanentChats &&
|
||||
builder: (context) =>
|
||||
!(widget.disableDismissForPermanentChats &&
|
||||
!chat.canBeDeleted)
|
||||
? Dismissible(
|
||||
confirmDismiss: (_) async =>
|
||||
widget.deleteChatDialog
|
||||
?.call(context, chat) ??
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (BuildContext context) =>
|
||||
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(
|
||||
_deleteDialog(
|
||||
chat,
|
||||
translations,
|
||||
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: (_) {
|
||||
setState(() {
|
||||
|
@ -267,14 +169,28 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
});
|
||||
widget.onDeleteChat(chat);
|
||||
},
|
||||
background: ColoredBox(
|
||||
secondaryBackground: const ColoredBox(
|
||||
color: Colors.red,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
translations.deleteChatButton,
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -381,6 +370,7 @@ class ChatListItem extends StatelessWidget {
|
|||
)
|
||||
: null,
|
||||
),
|
||||
context,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -50,7 +50,6 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
|||
appBar: AppBar(
|
||||
iconTheme: theme.appBarTheme.iconTheme ??
|
||||
const IconThemeData(color: Colors.white),
|
||||
backgroundColor: theme.appBarTheme.backgroundColor,
|
||||
title: _buildSearchField(),
|
||||
actions: [
|
||||
_buildSearchIcon(),
|
||||
|
@ -58,47 +57,32 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
|||
),
|
||||
body: Column(
|
||||
children: [
|
||||
if (widget.showGroupChatButton) ...[
|
||||
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: [
|
||||
if (widget.showGroupChatButton && !_isSearching) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16.0,
|
||||
left: 32,
|
||||
right: 32,
|
||||
top: 20,
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.group,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
// Handle group chat creation
|
||||
child: FilledButton(
|
||||
onPressed: () async {
|
||||
await widget.onPressCreateGroupChat();
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.groups,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text(
|
||||
widget.translations.newGroupChatButton,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
style: theme.textTheme.displayLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -138,19 +122,20 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
|||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.translations.searchPlaceholder,
|
||||
hintStyle: theme.inputDecorationTheme.hintStyle,
|
||||
hintStyle:
|
||||
theme.textTheme.bodyMedium!.copyWith(color: Colors.white),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: theme.inputDecorationTheme.hintStyle,
|
||||
style: theme.textTheme.bodySmall!.copyWith(color: Colors.white),
|
||||
cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white,
|
||||
)
|
||||
: Text(
|
||||
widget.translations.newChatTitle,
|
||||
style: theme.appBarTheme.titleTextStyle,
|
||||
style: theme.textTheme.headlineLarge,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -205,51 +190,14 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
|||
.noUsersPlaceholderBuilder(widget.translations, context);
|
||||
}
|
||||
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,
|
||||
itemBuilder: (context, index) {
|
||||
var user = filteredUsers[index];
|
||||
return DecoratedBox(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
if (!isPressed) {
|
||||
isPressed = true;
|
||||
|
@ -257,9 +205,24 @@ class _NewChatScreenState extends State<NewChatScreen> {
|
|||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,17 @@
|
|||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
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_picker_popup.dart";
|
||||
|
||||
class NewGroupChatOverviewScreen extends StatefulWidget {
|
||||
const NewGroupChatOverviewScreen({
|
||||
required this.options,
|
||||
required this.onPressCompleteGroupChatCreation,
|
||||
required this.service,
|
||||
required this.users,
|
||||
this.translations = const ChatTranslations.empty(),
|
||||
super.key,
|
||||
});
|
||||
|
@ -18,8 +20,12 @@ class NewGroupChatOverviewScreen extends StatefulWidget {
|
|||
final ChatOptions options;
|
||||
final ChatTranslations translations;
|
||||
final ChatService service;
|
||||
final List<ChatUserModel> users;
|
||||
final Function(List<ChatUserModel>, String) onPressCompleteGroupChatCreation;
|
||||
final Function(
|
||||
List<ChatUserModel> users,
|
||||
String groupchatName,
|
||||
String? groupchatBio,
|
||||
Uint8List? imageBytes,
|
||||
) onPressCompleteGroupChatCreation;
|
||||
|
||||
@override
|
||||
State<NewGroupChatOverviewScreen> createState() =>
|
||||
|
@ -28,15 +34,24 @@ class NewGroupChatOverviewScreen extends StatefulWidget {
|
|||
|
||||
class _NewGroupChatOverviewScreenState
|
||||
extends State<NewGroupChatOverviewScreen> {
|
||||
final TextEditingController _textEditingController = TextEditingController();
|
||||
final TextEditingController _chatNameController = TextEditingController();
|
||||
final TextEditingController _bioController = TextEditingController();
|
||||
Uint8List? image;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = Theme.of(context);
|
||||
var formKey = GlobalKey<FormState>();
|
||||
var isPressed = false;
|
||||
var users = widget.service.chatOverviewService.currentlySelectedUsers;
|
||||
|
||||
void onUploadImage(groupImage) {
|
||||
setState(() {
|
||||
image = groupImage;
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
appBar: AppBar(
|
||||
iconTheme: theme.appBarTheme.iconTheme ??
|
||||
const IconThemeData(color: Colors.white),
|
||||
|
@ -46,15 +61,113 @@ class _NewGroupChatOverviewScreenState
|
|||
style: theme.appBarTheme.titleTextStyle,
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
body: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: TextFormField(
|
||||
controller: _textEditingController,
|
||||
child: Column(
|
||||
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(
|
||||
fillColor: Colors.white,
|
||||
filled: true,
|
||||
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) {
|
||||
if (value == null || value.isEmpty) {
|
||||
|
@ -65,27 +178,143 @@ class _NewGroupChatOverviewScreenState
|
|||
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(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
onPressed: () async {
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
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) {
|
||||
isPressed = true;
|
||||
if (formKey.currentState!.validate()) {
|
||||
await widget.onPressCompleteGroupChatCreation(
|
||||
widget.users,
|
||||
_textEditingController.text,
|
||||
users,
|
||||
_chatNameController.text,
|
||||
_bioController.text,
|
||||
image,
|
||||
);
|
||||
}
|
||||
isPressed = false;
|
||||
}
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.check_circle,
|
||||
}
|
||||
: null,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ class NewGroupChatScreen extends StatefulWidget {
|
|||
|
||||
class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
|
||||
final FocusNode _textFieldFocusNode = FocusNode();
|
||||
List<ChatUserModel> selectedUserList = [];
|
||||
|
||||
bool _isSearching = false;
|
||||
String query = "";
|
||||
|
@ -51,19 +50,19 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
|
|||
} else if (snapshot.hasError) {
|
||||
return Text("Error: ${snapshot.error}");
|
||||
} 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();
|
||||
},
|
||||
),
|
||||
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(
|
||||
hintText: widget.translations.searchPlaceholder,
|
||||
hintStyle: theme.inputDecorationTheme.hintStyle,
|
||||
hintStyle:
|
||||
theme.textTheme.bodyMedium!.copyWith(color: Colors.white),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: theme.inputDecorationTheme.hintStyle,
|
||||
style: theme.textTheme.bodySmall!.copyWith(color: Colors.white),
|
||||
cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white,
|
||||
)
|
||||
: Text(
|
||||
|
@ -140,9 +140,74 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
|
|||
|
||||
return UserList(
|
||||
filteredUsers: filteredUsers,
|
||||
selectedUserList: selectedUserList,
|
||||
options: widget.options,
|
||||
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 {
|
||||
const UserList({
|
||||
required this.filteredUsers,
|
||||
required this.selectedUserList,
|
||||
required this.options,
|
||||
required this.translations,
|
||||
required this.service,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<ChatUserModel> filteredUsers;
|
||||
final List<ChatUserModel> selectedUserList;
|
||||
final ChatOptions options;
|
||||
final ChatTranslations translations;
|
||||
final ChatService service;
|
||||
|
||||
@override
|
||||
State<UserList> createState() => _UserListState();
|
||||
|
@ -167,75 +232,70 @@ class UserList extends StatefulWidget {
|
|||
|
||||
class _UserListState extends State<UserList> {
|
||||
@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,
|
||||
itemBuilder: (context, index) {
|
||||
var user = widget.filteredUsers[index];
|
||||
var isSelected = widget.selectedUserList
|
||||
var isSelected = widget
|
||||
.service.chatOverviewService.currentlySelectedUsers
|
||||
.any((selectedUser) => selectedUser == user);
|
||||
var theme = Theme.of(context);
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.colorScheme.secondary.withOpacity(0.3),
|
||||
width: 0.5,
|
||||
return widget.options.chatRowContainerBuilder(
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
widget.options.userAvatarBuilder(user, 44),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Text(
|
||||
user.fullName ?? widget.translations.anonymousUser,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (widget.selectedUserList.contains(user)) {
|
||||
widget.selectedUserList.remove(user);
|
||||
if (widget
|
||||
.service.chatOverviewService.currentlySelectedUsers
|
||||
.contains(user)) {
|
||||
widget.service.chatOverviewService
|
||||
.removeCurrentlySelectedUser(user);
|
||||
} 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,11 +23,39 @@ class DateFormatter {
|
|||
.inDays ==
|
||||
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({
|
||||
required DateTime date,
|
||||
bool showFullDate = false,
|
||||
}) =>
|
||||
DateFormat(
|
||||
_isToday(date) ? "HH:mm" : 'dd-MM-yyyy${showFullDate ? ' HH:mm' : ''}',
|
||||
).format(date);
|
||||
}) {
|
||||
if(showFullDate) {
|
||||
return DateFormat("dd - MM - yyyy HH:mm").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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
name: flutter_chat_view
|
||||
description: A standard flutter package.
|
||||
version: 3.0.1
|
||||
version: 3.1.0
|
||||
|
||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
|
||||
|
@ -25,7 +25,7 @@ dependencies:
|
|||
version: ^1.0.5
|
||||
flutter_profile:
|
||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
version: ^1.3.0
|
||||
version: ^1.5.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in a new issue