Code With Bisky

Build an Advanced Chat Group App with Appwrite | Delete, Read, Seen Messages + More Tutorial [25]

Key Topics covered:

  • Delete Messages
  • View Message Information
  • Update Group Picture
  • Implement Read and Seen Messages
  • Typing Status
  • Real-time updates

Description:

In this tutorial, we'll guide you through the process of building a fully-featured chat group application using the powerful Appwrite backend service. With step-by-step instructions and hands-on coding, you'll be able to create a dynamic and engaging chat experience for your users.

We will start by creating models

Models:

  • lib/model
  • GroupAppwrite.dart
  • GroupMemberAppwrite.dart
  • CreateGroupState.dart
  • GroupDetailsState.dart
  • MessageAppwriteDocument.dart
  • GroupMessageDetailsState.dart
  • GroupMessageInfoAppwrite.dart
  • GroupMessageState.dart
  • MyGroupsState.dart
  • GroupUserModel.dart

Code Snippet(GroupAppwrite.dart):


import 'package:json_annotation/json_annotation.dart';
part 'GroupAppwrite.g.dart';
@JsonSerializable()
class GroupAppwrite {

  String? id;
  String? message;
  String? messageType;
  String? messageId;
  DateTime? sendDate;
  DateTime? createdDate;
  String? name;
  String? description;
  String? creatorUserId;
  String? pictureName;
  String? pictureStorageId;
  bool? delivered;
  bool? read;
  String? sendUserId;


  GroupAppwrite({this.id,this.message,this.messageType,this.messageId,
    this.sendDate, this.createdDate, this.name, this.description, this.creatorUserId,
    this.delivered, this.read,this.pictureName, this.pictureStorageId,this.sendUserId});

  factory GroupAppwrite.fromJson(Map<String, dynamic> json) =>
      _$GroupAppwriteFromJson(json);

  Map<String, dynamic> toJson() => _$GroupAppwriteToJson(this);


}
 

Code Snippet(GroupMemberAppwrite.dart):


import 'package:json_annotation/json_annotation.dart';

part 'GroupMemberAppwrite.g.dart';

@JsonSerializable()
class GroupMemberAppwrite {

  String? memberUserId;
  String? name;
  bool? admin;
  bool? blocked;
  String? createdUserId;
  String? groupId;
  DateTime? dateJoined;

  GroupMemberAppwrite({this.memberUserId,this.name,this.admin,this.blocked,this.createdUserId,this.dateJoined,this.groupId});

  factory GroupMemberAppwrite.fromJson(Map<String, dynamic> json) =>
      _$GroupMemberAppwriteFromJson(json);

  Map<String, dynamic> toJson() => _$GroupMemberAppwriteToJson(this);

}

 

Code Snippet(MessageAppwriteDocument.dart):


import 'package:appwrite/models.dart';
import 'package:chat_with_bisky/model/MessageAppwrite.dart';

class MessageAppwriteDocument {

  MessageAppwrite? messageAppwrite;
  Document? document;



  MessageAppwriteDocument({this.messageAppwrite,this.document});




}

 

Code Snippet(GroupUserModel.dart):


import 'package:json_annotation/json_annotation.dart';

part 'GroupUserModel.g.dart';

@JsonSerializable()
class GroupUserModel {

  String? memberUserId;
  bool? selected;
  String? base64Image;
  String? name;

  GroupUserModel({this.memberUserId, this.base64Image,this.selected,this.name});

  factory GroupUserModel.fromJson(Map<String, dynamic> json) =>
      _$GroupUserModelFromJson(json);

  Map<String, dynamic> toJson() => _$GroupUserModelToJson(this);


}

 

Code Snippet(GroupMessageInfoAppwrite.dart):


import 'package:json_annotation/json_annotation.dart';

part 'GroupMessageInfoAppwrite.g.dart';

@JsonSerializable()
class GroupMessageInfoAppwrite {
  String? id;
  String? groupId;
  String? messageId;
  String? memberUserId;
  bool? delivered;
  bool? read;
  DateTime? deliveredTime;
  DateTime? readTime;

  GroupMessageInfoAppwrite(
      {this.groupId,
      this.id,
      this.messageId,
      this.memberUserId,
      this.delivered,
      this.read,
      this.deliveredTime,
      this.readTime});

  factory GroupMessageInfoAppwrite.fromJson(Map<String, dynamic> json) =>
      _$GroupMessageInfoAppwriteFromJson(json);

  Map<String, dynamic> toJson() => _$GroupMessageInfoAppwriteToJson(this);
}


 

Code Snippet(GroupMessageDetailsState.dart):


import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupMemberAppwrite.dart';
import 'package:chat_with_bisky/model/GroupMessageInfoAppwrite.dart';
import 'package:chat_with_bisky/model/db/MessageRealm.dart';
import 'package:flutter/services.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'GroupMessageDetailsState.freezed.dart';

@Freezed(makeCollectionsUnmodifiable:false)
class GroupMessageDetailsState with _$GroupMessageDetailsState {
  factory GroupMessageDetailsState({
    @Default('') String myUserId,
    @Default(false) bool loading,
    @Default(null) List<GroupMemberAppwrite>? members,
    @Default(null) List<GroupMessageInfoAppwrite>? deliveredToMembers,
    @Default(null) List<GroupMessageInfoAppwrite>? readsToMembers,
    @Default(null) GroupAppwrite? group,
    @Default(null) MessageRealm? message,
  }) = _GroupMessageDetailsState;
}


 

Code Snippet(GroupMessageState.dart):


import 'package:appwrite/models.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/MessageAppwrite.dart';
import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:chat_with_bisky/model/db/MessageRealm.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'GroupMessageState.freezed.dart';

@Freezed(makeCollectionsUnmodifiable:false)
class GroupMessageState with _$GroupMessageState {
  factory GroupMessageState({
    @Default('') String myUserId,
    @Default('') String message,
    @Default('') String messageType,
    @Default(false) bool loading,
    @Default(null) List<MessageRealm>? messages,
    @Default(null) File? file,
    @Default('') String groupDetails,
    @Default('') String groupId,
    @Default(null) GroupAppwrite? group,
    @Default(null) UserAppwrite? user,

  }) = _GroupMessageState;
}


 

Code Snippet(MyGroupsState.dart):


import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupUserModel.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'MyGroupsState.freezed.dart';

@Freezed(makeCollectionsUnmodifiable:false)
class MyGroupsState with _$MyGroupsState {
  factory MyGroupsState({
    @Default('') String myUserId,
    @Default(false) bool loading,
    @Default(null) List<GroupAppwrite>? groups,
  }) = _MyGroupsState;
}


 

Code Snippet(CreateGroupState.dart):


import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupUserModel.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'CreateGroupState.freezed.dart';

@Freezed(makeCollectionsUnmodifiable:false)
class CreateGroupState with _$CreateGroupState {
  factory CreateGroupState({
    @Default('') String myUserId,
    @Default(false) bool loading,
    @Default(null) GroupAppwrite? group,
    @Default(null) List<GroupUserModel>? members,
    @Default(null) List<GroupUserModel>? allFriends,
  }) = _CreateGroupState;
}

 

Code Snippet(GroupDetailsState.dart):


import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupMemberAppwrite.dart';
import 'package:flutter/services.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'GroupDetailsState.freezed.dart';

@Freezed(makeCollectionsUnmodifiable:false)
class GroupDetailsState with _$GroupDetailsState {
  factory GroupDetailsState({
    @Default('') String myUserId,
    @Default(false) bool loading,
    @Default(null) List<GroupMemberAppwrite>? members,
    @Default(null) GroupAppwrite? group,
    @Default(null) String? fileName,
    @Default(null) Uint8List? image,
  }) = _GroupDetailsState;
}


 

Code Snippet(AttachmentType.dart) modification:


  static messageType(String type) {
    if (type == AttachmentType.text) {
      return 'Text Message';
    } else if (type == AttachmentType.image) {
      return 'Image Message';
    } else if (type == AttachmentType.video) {
      return 'Video Message';
    }else if (type == AttachmentType.voice) {
      return 'Voice Message';
    }
    return '';
  }
 

Run the following command flutter packages pub run build_runner build

We need to create 3 collections with the same attributes of the models we created on AppWrite Dashboard.

  • groups >> GroupAwppwrite.dart
  • group_members >> GroupMemberAppwrite.dart
  • group_messages_info >> GroupMessageInfoAppwrite.dart

After Creating collections, we need to add permissions of each All Users, as shown on the image below.

Appwrite Permissions

Add the following indexes for group_members collection. Please take a note on the attributes section

Group Chat Members Indexes

Add the following indexes for group_messages_info collection

Group Chat Messages Info

Let's add the following providers. Remember that we are using riverpod state management

Providers:

  • lib/core/providers
  • FileTempProvider.dart - Responsible for downloading file and save it temporally on the device
  • GetGroupMessageInfoProvider.dart - Get Group messages Information Asynchronously
  • GetUserProvider.dart - Get User Asynchronously
  • GroupMessagesInfoRepositoryProvider.dart - Managing Group Messages Information Business Logic
  • GroupRepositoryProvider.dart - Managing Groups (Business Logic)
  • ProfileRepositoryProvider.dart - Managing Group Profile (Business Logic)

Code Snippet(FileTempProvider.dart):




import 'dart:io';

import 'package:chat_with_bisky/core/providers/StorageProvider.dart';
import 'package:path_provider/path_provider.dart';

import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:appwrite/models.dart' as model;
part 'FileTempProvider.g.dart';
@riverpod
Future<File> fileTemp(FileTempRef ref,
String bucketId,
String id) async{

  ref.keepAlive();
  final storageProviderRef =  ref.read(storageProvider);
  model.File fileAppwrite = await storageProviderRef.getFile(bucketId: bucketId, fileId: id);

  final dir =await getTemporaryDirectory();
  final file = File('${dir.path}/${fileAppwrite.name}');
  if(await file.exists()){
    return file;
  }
  final dataResponse = await storageProviderRef.getFileDownload(bucketId: bucketId, fileId: id);
  await file.writeAsBytes(dataResponse);
  return file;
}


 

Code Snippet(GetGroupMessageInfoProvider.dart):


import 'package:chat_with_bisky/core/providers/GroupMessagesInfoRepositoryProvider.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupMessageInfoAppwrite.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'GetGroupMessageInfoProvider.g.dart';

@riverpod
Future<GroupMessageInfoAppwrite?> getGroupMessageInfo(
    GetGroupMessageInfoRef ref, GroupAppwrite group, String myUserId) async {
  ref.keepAlive();
  final groupMessagesInfoRepository =
      ref.read(groupMessagesInfoRepositoryProvider);
  final info = await groupMessagesInfoRepository.getOrCreateGroupMessageInfo(
      group.messageId ?? "", myUserId, group);
  return info;
}


 

Code Snippet(GetUserProvider.dart):





import 'package:chat_with_bisky/core/providers/UserRepositoryProvider.dart';
import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'GetUserProvider.g.dart';

@riverpod
Future<UserAppwrite?> getUser(GetUserRef ref,
    String id) async{

  ref.keepAlive();
  final userRepository = ref.read(userRepositoryProvider);
  final user = await userRepository.getUser(id);
  return user;
}


 

Code Snippet(GroupMessagesInfoRepositoryProvider.dart):


import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/providers/DatabaseProvider.dart';
import 'package:chat_with_bisky/core/providers/GroupRepositoryProvider.dart';
import 'package:chat_with_bisky/core/providers/MessageRepositoryProvider.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupMessageInfoAppwrite.dart';
import 'package:chat_with_bisky/model/MessageAppwrite.dart';
import 'package:chat_with_bisky/model/MessageAppwriteDocument.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:realm/realm.dart';

final groupMessagesInfoRepositoryProvider = Provider(
  (ref) => GroupMessagesInfoRepositoryProvider(ref),
);

class GroupMessagesInfoRepositoryProvider {
  final Ref _ref;

  Databases get _databases => _ref.read(databaseProvider);
  GroupRepositoryProvider get _groupRepositoryProvider => _ref.read(groupRepositoryProvider);
  MessageRepositoryProvider get _messageRepositoryProvider => _ref.read(messageRepositoryProvider);

  GroupMessagesInfoRepositoryProvider(this._ref);

  Future<void> updateGroupMemberMessagesInfoSeen(String groupId,
      String myUserId, String messageId) async {
    try {

      GroupAppwrite? group =await _groupRepositoryProvider.getGroup(groupId);
      if(group == null){
        return;
      }
      GroupMessageInfoAppwrite? groupMessageInfoAppwrite =
          await getOrCreateGroupMessageInfo(
  messageId, myUserId, group);

      if(groupMessageInfoAppwrite == null || (groupMessageInfoAppwrite.read == true)){
        checkAndUpdateGroupIfEveryMemberReadOrReceivedMessage(group,messageId,myUserId);
        return;
      }
     await _databases.updateDocument(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupMessagesInfoId,
          documentId: groupMessageInfoAppwrite.id!,
          data: {
'delivered': true,
'read': true
          });
      checkAndUpdateGroupIfEveryMemberReadOrReceivedMessage(group,messageId,myUserId);
    } catch (e) {
      print('updateGroupMemberMessagesInfoSeen $e');
    }
  }

  Future<void> updateGroupMemberMessagesInfoDelivered(
      GroupAppwrite groupAppwrite,
      String memberUserId,
      String messageId) async {
    try {
      GroupMessageInfoAppwrite? groupMessageInfoAppwrite =
          await getOrCreateGroupMessageInfo(
  messageId, memberUserId, groupAppwrite);

      if(groupMessageInfoAppwrite == null || ( groupMessageInfoAppwrite.delivered == true)){
        return;
      }
      await _databases.updateDocument(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupMessagesInfoId,
          documentId: groupMessageInfoAppwrite.id!,
          data: {
'delivered': true
          });

      checkAndUpdateGroupIfEveryMemberReadOrReceivedMessage(groupAppwrite,messageId,memberUserId);
    } catch (e) {
      print('updateGroupMemberMessagesInfoDelivered $e');
    }
  }

  Future<GroupMessageInfoAppwrite?> getOrCreateGroupMessageInfo(
      String messageId,
      String memberUserId,
      GroupAppwrite groupAppwrite) async {
    GroupMessageInfoAppwrite? retrieved =
        await getGroupMessagesInfo(messageId, memberUserId);
    if (retrieved == null) {
      return await createGroupMessagesInfo(
          groupAppwrite, memberUserId, messageId);
    }
    return retrieved;
  }

  Future<GroupMessageInfoAppwrite?> createGroupMessagesInfo(
      GroupAppwrite groupAppwrite,
      String memberUserId,
      String messageId) async {

    if(groupAppwrite.sendUserId == memberUserId){
      return null;
    }
    try {
      String id = ObjectId().hexString;
      GroupMessageInfoAppwrite groupMessageInfoAppwrite =
          GroupMessageInfoAppwrite(
  read: false,
  delivered: false,
  memberUserId: memberUserId,
  messageId: messageId,
  groupId: groupAppwrite.id,
  id: id);

      GroupMessageInfoAppwrite? retrieved =
          await getGroupMessagesInfo(messageId, memberUserId);
      if (retrieved != null) {
        return retrieved;
      }

      Document document = await _databases.createDocument(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupMessagesInfoId,
          documentId: id,
          data: groupMessageInfoAppwrite.toJson());
      return GroupMessageInfoAppwrite.fromJson(document.data);
    } on AppwriteException catch (exception) {

      if(exception.code != 409){
        print('Exception createGroupMessagesInfo $exception');
      }

    }
    return null;
  }

  Future<GroupMessageInfoAppwrite?> getGroupMessagesInfo(
      String messageId, String memberUserId) async {
    try {
      DocumentList documentList = await _databases.listDocuments(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupMessagesInfoId,
          queries: [
Query.equal("messageId", [messageId]),
Query.equal("memberUserId", [memberUserId]),
Query.orderDesc('\$createdAt'),
Query.limit(100),
          ]);

      if (documentList.total > 0) {
        return GroupMessageInfoAppwrite.fromJson(
documentList.documents.first.data);
      }
    } on AppwriteException catch (exception) {
      print('Exception getGroupMessagesInfo $exception');
    }

    return null;
  }

  Future<void> checkAndUpdateGroupIfEveryMemberReadOrReceivedMessage(GroupAppwrite group, String messageId, String memberUserId) async {


    final grp = await _groupRepositoryProvider.getGroup(group.id??"");

    final members = await _groupRepositoryProvider.getGroupMembers(group.id??"");
    int countMembers = members.length;
    countMembers = countMembers -1;
    final countReads = await getGroupMessageReads(messageId);
    if(countMembers == countReads.length ){
      group.delivered = true;
      group.read = true;
      if(grp?.messageId == messageId){
        _groupRepositoryProvider.updateGroup(group);
      }
      _messageRepositoryProvider.updateMessageSeen(messageId);
    } else{
      if(group.delivered == true){
        return;
      }
      final countDelivered = await getGroupMessageDelivered(messageId);

      if(countMembers == countDelivered.length){
        group.delivered = true;
        group.read = false;
        if(grp?.messageId == messageId){
          _groupRepositoryProvider.updateGroup(group);
        }
        _messageRepositoryProvider.updateMessageDelivered(messageId);
      }
    }
  }



  Future< List<GroupMessageInfoAppwrite>> getGroupMessageReads(String messageId) async {
    try {
      DocumentList documentList = await _databases.listDocuments(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupMessagesInfoId,
          queries: [
Query.equal("messageId", [messageId]),
Query.equal("read", [true])
          ]);

      if(documentList.total >0){

        List<GroupMessageInfoAppwrite> list = [];
        for (var element in documentList.documents) {
          list.add(GroupMessageInfoAppwrite.fromJson(element.data));
        }

        return list;
      }



    } on AppwriteException catch (exception) {
      print('Exception getGroupMessageReads $exception');
    }
    return [];
  }

  Future<List<GroupMessageInfoAppwrite>> getGroupMessageDelivered(String messageId) async {
    try {
      DocumentList documentList = await _databases.listDocuments(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupMessagesInfoId,
          queries: [
Query.equal("messageId", [messageId]),
Query.equal("delivered", [true])
          ]);
      if(documentList.total >0){

        List<GroupMessageInfoAppwrite> list = [];
        for (var element in documentList.documents) {
          list.add(GroupMessageInfoAppwrite.fromJson(element.data));
        }

        return list;
      }
    } on AppwriteException catch (exception) {
      print('Exception getGroupMessageReads $exception');
    }
    return [];
  }


}


 

Code Snippet(GroupRepositoryProvider.dart):


import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/providers/DatabaseProvider.dart';
import 'package:chat_with_bisky/core/providers/UserRepositoryProvider.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupMemberAppwrite.dart';
import 'package:chat_with_bisky/model/GroupUserModel.dart';
import 'package:chat_with_bisky/model/MessageAppwrite.dart';
import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:chat_with_bisky/service/LocalStorageService.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:realm/realm.dart';

final groupRepositoryProvider = Provider(
  (ref) => GroupRepositoryProvider(ref),
);

class GroupRepositoryProvider {
  final Ref _ref;

  Databases get _databases => _ref.read(databaseProvider);

  UserDataRepositoryProvider get _userRepositoryProvider =>
      _ref.read(userRepositoryProvider);

  GroupRepositoryProvider(this._ref);

  Future<bool?> createGroup(
      GroupAppwrite groupAppwrite, List<GroupUserModel> members) async {
    try {
      String userId =
          await LocalStorageService.getString(LocalStorageService.userId) ?? "";
      String id = ObjectId().hexString;
      groupAppwrite.id = id;
      groupAppwrite.creatorUserId = userId;
      UserAppwrite? userAppwrite =
      await _userRepositoryProvider.getUser(userId);

      if(userAppwrite == null){
        return false;
      }
      groupAppwrite.sendUserId = userId;
      Document document = await _databases.createDocument(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupId,
          documentId: id,
          data: groupAppwrite.toJson());

      return await createGroupMembers(groupAppwrite, document, members, userId,userAppwrite);
    } catch (e) {
      print('createGroup $e');
    }
    return false;
  }

  Future<bool> createGroupMembers(GroupAppwrite group, Document groupDocument,
      List<GroupUserModel> members, String userId,UserAppwrite? userAppwrite) async {
    try {
      List<GroupMemberAppwrite> appwriteGroupMembers = [];
      for (var element in members) {
        final mbr = GroupMemberAppwrite(
memberUserId: element.memberUserId,
admin: false,
blocked: false,
createdUserId: userId,
name: element.name,
dateJoined: DateTime.parse(groupDocument.$createdAt),
groupId: groupDocument.$id);
        appwriteGroupMembers.add(mbr);
      }

      if (userAppwrite != null) {
        final adminMember = GroupMemberAppwrite(
memberUserId: userId,
admin: true,
blocked: false,
createdUserId: userId,
name: userAppwrite.name,
dateJoined: DateTime.parse(groupDocument.$createdAt),
groupId: groupDocument.$id);
        appwriteGroupMembers.add(adminMember);

        for (var element in appwriteGroupMembers) {
          await _databases.createDocument(
  databaseId: Strings.databaseId,
  collectionId: Strings.collectionGroupMembersId,
  documentId: ObjectId().hexString,
  data: element.toJson());
        }

        return true;
      }

      return false;
    } catch (e) {
      print('createGroupMembers $e');
    }

    return false;
  }


  Future<GroupMemberAppwrite?> getGroupMemberByUserId(String groupId,String memberUserId) async {
    try {
      DocumentList documentList = await _databases.listDocuments(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupMembersId,
          queries: [
Query.equal("groupId", [groupId]),
Query.equal("memberUserId", [memberUserId]),
          ]);

      if (documentList.total > 0) {

        GroupMemberAppwrite groupMemberAppwrite = GroupMemberAppwrite.fromJson(documentList.documents.first.data);
        return groupMemberAppwrite;
      }
    } on AppwriteException catch (exception) {
      print('getGroupMemberByUserId Exception \n $exception');
    }
    return null;
  }


  Future<GroupAppwrite?> getGroup(String id) async {

    try {
      Document document = await _databases.getDocument(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupId,
          documentId: id);

      return GroupAppwrite.fromJson(document.data);
    } on AppwriteException catch (e) {
      print('ERROR getGroup $e');
    } catch (e) {
      print('ERROR getGroup $e');
    }
    return null;
  }



  Future<List<GroupAppwrite>> getMemberGroups(String memberUserId) async {

    try {

      DocumentList documentList = await _databases.listDocuments(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupMembersId,
          queries: [
Query.equal("memberUserId", [memberUserId]),
          ]);

      if (documentList.total > 0) {
        List<GroupAppwrite>  groups = [];
        for (Document document in documentList.documents) {
          GroupMemberAppwrite groupMember = GroupMemberAppwrite.fromJson(document.data);
          GroupAppwrite? group  = await getGroup(groupMember.groupId??"");
          if(group != null){
groups.add(group);
          }
        }

        return groups;

      }
    } on AppwriteException catch (e) {
      print('ERROR getMemberGroups $e');
    } catch (e) {
      print('ERROR getMemberGroups $e');
    }
    return [];
  }


  Future<List<GroupMemberAppwrite>> getGroupMembers(String groupId) async {

    try {
      DocumentList documentList = await _databases.listDocuments(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupMembersId,
          queries: [
Query.equal("groupId", [groupId]),
          ]);

      if (documentList.total > 0) {
        List<GroupMemberAppwrite>  groupsMembers = [];
        for (Document document in documentList.documents) {
          GroupMemberAppwrite groupMember = GroupMemberAppwrite.fromJson(document.data);
          groupsMembers.add(groupMember);
        }
        return groupsMembers;
      }
    } on AppwriteException catch (e) {
      print('ERROR getGroupMembers $e');
    } catch (e) {
      print('ERROR getGroupMembers $e');
    }
    return [];
  }

  Future<bool> updateGroup(GroupAppwrite group) async {

    try {


      Document document = await _databases.updateDocument(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupId,
          documentId: group.id??"",
      data: group.toJson());

      return true;

    } on AppwriteException catch (exception) {
      print('update group $exception');

    }

    return false;
  }


  Future<bool> updateGroupProfilePicture(GroupAppwrite group,String imageId) async {

    try {


      Document document = await _databases.updateDocument(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupId,
          documentId: group.id??"",
          data: {
'pictureStorageId':imageId
          }
      );

      return true;

    } on AppwriteException catch (exception) {
      print('update updateGroupProfilePicture $exception');

    }
    return false;
  }



  Future<bool> updateGroupName(GroupAppwrite group,String name) async {

    try {


      Document document = await _databases.updateDocument(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionGroupId,
          documentId: group.id??"",
          data: {
'name':name
          }
      );

      return true;

    } on AppwriteException catch (exception) {
      print('update updateGroupProfilePicture $exception');

    }
    return false;
  }

}


 

Code Snippet(ProfileRepositoryProvider.dart):


import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/providers/DatabaseProvider.dart';
import 'package:chat_with_bisky/core/providers/GroupRepositoryProvider.dart';
import 'package:chat_with_bisky/core/providers/StorageProvider.dart';
import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

final profileRepositoryProvider =
    Provider((ref) => ProfileRepositoryProvider(ref));

class ProfileRepositoryProvider {
  final Ref _ref;
  Storage get _storage => _ref.read(storageProvider);
  GroupRepositoryProvider get _groupRepositoryProvider => _ref.read(groupRepositoryProvider);

  ProfileRepositoryProvider(this._ref);


  Future<bool> uploadGroupProfilePicture(
      String? imageExist, String imageId, String path) async {
    try {
      if (imageExist != null) {
        await _storage.deleteFile(
bucketId: Strings.profilePicturesBucketId, fileId: imageExist);
      }
      File file = await _storage.createFile(
        bucketId: Strings.profilePicturesBucketId,
        fileId: imageId,
        file: InputFile(
path: path, filename: '$imageId${getFileExtension(path)}'),
      );

      return true;
    } on AppwriteException catch (exception) {
      print(exception);
    }
    return false;
  }

  String getFileExtension(String fileName) {
    try {
      return ".${fileName.split('.').last}";
    } catch (e) {
      return "";
    }
  }

  Future<Uint8List?> getExistingProfilePicture(String imageId) async {
    try {
      Uint8List uint8list = await _storage.getFilePreview(
          bucketId: Strings.profilePicturesBucketId, fileId: imageId);

      return uint8list;
    } on AppwriteException catch (exception) {
      print(exception);
    }
    return null;
  }
}


 

Code Snippet(MessageRepositoryProvider.dart) modification:


import 'package:chat_with_bisky/model/MessageAppwriteDocument.dart';

  Future<List<MessageAppwriteDocument>> getGroupMessages(String groupId) async {

    try {

      DocumentList documentList = await _databases.listDocuments(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionMessagesId,
          queries: [
Query.equal("receiverUserId", [groupId]),
Query.orderDesc('\$createdAt'),
Query.limit(100),
          ]);

      List<MessageAppwriteDocument> messages = [];
      if (documentList.total > 0) {
        for (Document document in documentList.documents) {
          MessageAppwrite message = MessageAppwrite.fromJson(document.data);
          message.sendDate = DateTime.parse(document.$createdAt);
          messages.add(MessageAppwriteDocument(messageAppwrite: message,document: document));
        }

        return messages;
      }
    } on AppwriteException catch (exception) {
      print(exception);
    }

    return [];

  }

  deleteMessage(String messageId) async {

    try {

       await _databases.deleteDocument(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionMessagesId,
          documentId: messageId);

      return true;
    } on AppwriteException catch (exception) {
      print('delete message $exception');
    }

    return false;
  }

 

Code Snippet(RealmProvider.dart) modification:


  List<FriendContactRealm> getMyFriends(String userId){
    RealmResults<FriendContactRealm> results = _realm.query<FriendContactRealm>(r'userId = $0 SORT(displayName ASC)',[userId]);
    if(results.isNotEmpty){
      return results.toList();
    }
    return [];
  }
  List<ChatRealm> getMyChats(String userId){
    RealmResults<ChatRealm> results = _realm.query<ChatRealm>(r'receiverUserId = $0 SORT(displayName ASC)',[userId]);
    if(results.isNotEmpty){
      return results.toList();
    }
    return [];
  }

 

Code Snippet(RealtimeNotifierProvider.dart) modification:


'databases.${Strings.databaseId}.collections.${Strings.collectionGroupId}.documents',
'databases.${Strings.databaseId}.collections.${Strings.collectionGroupMembersId}.documents',
 

Code Snippet(lib/constant/strings.dart):


static const collectionGroupId = "64d54d3e4539c121267a";
static const collectionGroupMembersId = "64d54e6e4c46a65575ac";
static const collectionGroupMessagesInfoId = "64d7cc33305fb9fd71d5";
 

Code Snippet(pubspecyaml.dart):


//add the following dependencies
dependencies:
    ........
  focus_detector: ^2.0.1
  shimmer: ^3.0.0
  circular_image: ^0.0.6
    
dev_dependencies:
    ........
  custom_lint: ^0.4.0
  riverpod_lint: ^1.3.2
 

Code Snippet(analysis_options.dart):

This will help to resolve riverpod errors and show suggestions during compile time


analyzer:
  plugins:
    - custom_lint
 

Run the following command flutter packages pub run build_runner build

Add Items widgets of the screens we created:

Let's add the following items

  • lib/widget
  • modify ChatMessageItem.dart
  • DefaultTempImage.dart - Widget For A Loading Temporary File Image
  • GroupChatMessageItem.dart - Widget For A Group Chat Message Item
  • GroupMemberTileWidget.dart - Widget For A Group Member Item
  • GroupTileItemWidget.dart - Widget For A Group List Item
  • UserImage.dart - Widget to show user/member Profile Image

Code Snippet(ChatMessageItem.dart):


// fix video type in method fileView, first line of children[] Stack. Add below code
if (type == AttachmentType.video)


 

Code Snippet(DefaultTempImage.dart):



import 'dart:io';

import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/providers/FileTempProvider.dart';
import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../core/providers/GetUserProvider.dart';

class DefaultTempImage extends HookWidget{


  String bucketId;
  String? imageStorageId;
  double? size;

  DefaultTempImage(this.bucketId,this.imageStorageId, {super.key,this.size=60});

  @override
  Widget build(BuildContext context) {

    return SizedBox(
      width: size?? 20,
      child: imageStorageId != null ?Consumer(builder: (context, ref, child) {
        final fileAsync = ref.watch(fileTempProvider(
bucketId,
imageStorageId!));
        return fileAsync.when(
          data: (file) => groupPicture(file),
          error: (error, stackTrace) {

return  defaultImage();
          },
          loading: () => const SizedBox(
  width: 20, child: LinearProgressIndicator()),
        );
      }):defaultImage(),
    );
  }

  CircleAvatar defaultImage() {
    return  CircleAvatar(
      radius: size??20.0,
      backgroundImage: const NetworkImage(Strings.avatarImageUrl),
    );
  }


  groupPicture(File file) {
    Uint8List uint8list = Uint8List.fromList(file.readAsBytesSync());
    return Padding(
      padding: const EdgeInsets.all(4.0),
      child: CircleAvatar(radius: size??20, backgroundImage: MemoryImage(uint8list)),
    );
  }



}


 

Code Snippet(GroupChatMessageItem.dart):


import 'dart:io';

import 'package:chat_bubbles/chat_bubbles.dart';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/extensions/extensions.dart';
import 'package:chat_with_bisky/core/providers/FileProvider.dart';
import 'package:chat_with_bisky/core/providers/FileTempProvider.dart';
import 'package:chat_with_bisky/core/providers/GetUserProvider.dart';
import 'package:chat_with_bisky/core/providers/GroupMessagesInfoRepositoryProvider.dart';
import 'package:chat_with_bisky/core/providers/MessageRepositoryProvider.dart';
import 'package:chat_with_bisky/core/providers/ThumbnailProvider.dart';
import 'package:chat_with_bisky/core/providers/UserRepositoryProvider.dart';
import 'package:chat_with_bisky/model/AttachmentType.dart';
import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:chat_with_bisky/model/db/MessageRealm.dart';
import 'package:chat_with_bisky/widget/AsynWidget.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:chat_with_bisky/model/MessageAppwrite.dart';

class GroupChatMessageItem extends HookConsumerWidget {
  final MessageRealm message;
  final bool myMessage;
  final String displayName;
  final String myUserId;
  ValueChanged<MessageRealm> messageLongPress;

   GroupChatMessageItem(
      {Key? key,
      required this.message,
      required this.myMessage,
      required this.displayName,
      required this.myUserId,
      required this.messageLongPress,
      })
      : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {

    final theme = Theme.of(context);
    final style = theme.textTheme;



    useEffect((){

      if(!myMessage && message.read != true){

         ref.read(groupMessagesInfoRepositoryProvider).updateGroupMemberMessagesInfoSeen(message.receiverUserId??"",myUserId,message.messageIdUpstream??"");

      }
      return null;

    });


    Widget fileView(File file, String type) {
      return Stack(
        fit: StackFit.passthrough,
        children: [
          ConstrainedBox(
  constraints: BoxConstraints(
    minHeight: 0,
    maxHeight:
        [AttachmentType.image, AttachmentType.video].contains(type)
? double.infinity
: (context.media.size.width * 3 / 4),
  ),
  child: Stack(
    children: [
      if (type == AttachmentType.video)
      AsyncWidget(
        value: ref.watch(thumbnailProvider(file.path)),
        data: (data) => data != null
? Stack(
          fit: StackFit.passthrough,
          alignment: Alignment.center,
          children: [
ClipRRect(
  borderRadius: BorderRadius.circular(12),
  child: BubbleNormalImage(
    onTap: (){

    },
    id: message.sendDate.toString(),
    image: Image.file(
      File(data),
      fit: BoxFit.cover,
    ),
    color: myMessage?const Color(0xFF1B97F3):const Color(0xFFE8E8EE),
    // tail: true,
    isSender: myMessage,
  ) ,
),
const Icon(
  Icons.play_arrow_rounded,
  size: 40,
  color: Colors.blue,
),
          ],
        )
: const SizedBox(),
      ),
      if (type == AttachmentType.image)
        ClipRRect(
          borderRadius: BorderRadius.circular(12),
          child:  BubbleNormalImage(
onTap: () =>  context.openImage(file.path),
id: message.sendDate.toString(),
image: Image.file(
  file,
  fit: BoxFit.cover,
),
color: myMessage?const Color(0xFF1B97F3):const Color(0xFFE8E8EE),
isSender: myMessage,
          ) ,
        )

    ],
  )),
        ],
      );
    }

    Widget messageWidget(MessageRealm messageAppwrite) {
      if (messageAppwrite.type == AttachmentType.text) {

        return BubbleNormal(
          text: messageAppwrite.message ?? "",
          isSender: myMessage,
          color: myMessage?const Color(0xFF1B97F3):const Color(0xFFE8E8EE),
          tail: true,
          textStyle: TextStyle(
fontSize: 12,
color:  myMessage?Colors.white:Colors.blue,
          ),
        );
      } else {
        return Consumer(
          builder: (context, ref, child) {
final fileAsync = ref.watch(fileProvider(Strings.messagesBucketId,
    messageAppwrite.message ?? "", messageAppwrite.fileName ?? ""));
return fileAsync.when(
  data: (file) => fileView(file, messageAppwrite.type ?? ""),
  error: (error, stackTrace) {
    print(error);
    return Center(
      child: Text('$error'),
    );
  },
  loading: () =>
      const SizedBox(width: 20, child: LinearProgressIndicator()),
);
          },
        );
      }
    }

    Widget buildChatLayout(MessageRealm snapshot) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
  mainAxisAlignment:
      myMessage ? MainAxisAlignment.end : MainAxisAlignment.start,
  children: <Widget>[
    const SizedBox(
      width: 10.0,
    ),
    Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          if(!myMessage) Consumer(builder: (context, ref, child) {
final userAsync = ref.watch(getUserProvider((message.senderUserId??"")));
return userAsync.when(
  data: (user) => userDetail(user, message),
  error: (error, stackTrace) {
    return Text(message.senderUserId ??"",style: const TextStyle(color: Colors.brown),);
  },
  loading: () =>
  const SizedBox(width: 20, child: LinearProgressIndicator()),
);
          },),
          GestureDetector(onLongPress: () => messageLongPress(message),
  child: messageWidget(snapshot))
        ],
      ),
    )
  ],
),
          ),
        ],
      );
    }

    return Padding(
      padding: const EdgeInsets.all(8.0),
        child: Container(
          decoration: BoxDecoration(
borderRadius: BorderRadius.only(
  topRight: Radius.circular(myMessage?0:16),
  topLeft: Radius.circular(myMessage?16:0),
  bottomLeft: const Radius.circular(16),
  bottomRight: const Radius.circular(16),

),
          ),
child: Column(
  children: [
    buildChatLayout(message),
    Row(
      mainAxisAlignment:
      myMessage? MainAxisAlignment.end: MainAxisAlignment.start,

      children: [
        if(myMessage) const Spacer(),

        Padding(padding: const EdgeInsets.all(4),
        child: Wrap(
          alignment: WrapAlignment.end,
          spacing: 8,
          crossAxisAlignment: WrapCrossAlignment.end,
          children: [
Text(message.sendDate!.getFormattedTime(),
style: style.labelSmall!.copyWith(
  color: Colors.grey,
  fontSize: 12,
  fontWeight: FontWeight.bold
),),
if(myMessage)
  Icon(message.read == true || message.delivered == true?
  Icons.done_all_rounded : Icons.done_rounded,
  size: 16,
      color: message.read == true? Colors.green.shade800:Colors.grey,)
          ],
        ),
        )
      ],
    )
  ],
)));
  }

  userDetail(UserAppwrite? user, MessageRealm message) {
    if(user != null){
      return Consumer(builder: (context, ref, child) {
        final fileAsync = ref.watch(fileTempProvider(Strings.profilePicturesBucketId,user.profilePictureStorageId??""));
        return fileAsync.when(
          data: (file) => memberProfilePicture(file,user),
          error: (error, stackTrace) {
return Text(user.name ?? message.senderUserId ??"",style: const TextStyle(color: Colors.brown),);
          },
          loading: () =>
          const SizedBox(width: 20, child: LinearProgressIndicator()),
        );
      });
    }
    return const SizedBox();
  }

  memberProfilePicture(File file, UserAppwrite user) {

    Uint8List uint8list = Uint8List.fromList(file.readAsBytesSync());
    return Row(
      children: [
    CircleAvatar(
backgroundImage: MemoryImage(uint8list)
        ),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text(user.name ?? message.senderUserId ??"",style: const TextStyle(color: Colors.orange,fontWeight: FontWeight.bold),),
        ),
      ],
    );
  }


}


 

Code Snippet(GroupMemberTileWidget.dart):


import 'package:chat_with_bisky/model/GroupMemberAppwrite.dart';
import 'package:chat_with_bisky/widget/UserImage.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class GroupMemberTileWidget extends HookConsumerWidget {
  GroupMemberAppwrite member;
  String userId;

  GroupMemberTileWidget(this.member, this.userId, {super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final theme = Theme.of(context);
    final style = theme.textTheme;
    return ListTile(
      leading: UserImage(member.memberUserId ?? ""),
      title: Text(
        member.name ?? "",
        maxLines: 1,
        style: style.labelSmall
?.copyWith(fontSize: 13, fontWeight: FontWeight.bold),
        overflow: TextOverflow.ellipsis,
      ),
      subtitle: Row(
        children: [
          Text((member.admin == true) ? "Admin" : 'Member'),
        ],
      ),
      onTap: () async {},
    );
  }
}


 

Code Snippet(GroupTileItemWidget.dart):


import 'package:auto_route/auto_route.dart';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/extensions/extensions.dart';
import 'package:chat_with_bisky/core/providers/GetGroupMessageInfoProvider.dart';
import 'package:chat_with_bisky/core/providers/GroupMessagesInfoRepositoryProvider.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/route/app_route/AppRouter.gr.dart';
import 'package:chat_with_bisky/widget/DefaultTempImage.dart';
import 'package:chat_with_bisky/widget/friend_image.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class GroupTileItemWidget extends ConsumerStatefulWidget {
  final GroupAppwrite group;
  final String userId;
  const GroupTileItemWidget({
    super.key,
    required this.group,
    required this.userId,
  });
  @override
  _GroupTileItemWidget createState() {

    return _GroupTileItemWidget();
  }
}
class _GroupTileItemWidget extends ConsumerState <GroupTileItemWidget>{

  @override
  void initState() {
    super.initState();

  }
  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final style = theme.textTheme;
    bool isRead = widget.group.read == true;

    return ListTile(
      leading: DefaultTempImage(Strings.profilePicturesBucketId,widget.group.pictureStorageId,size: 50,),
      title:  Row(
        // mainAxisAlignment: MainAxisAlignment.center,
        mainAxisSize: MainAxisSize.min,
        children: [
          Flexible(
child: Text(widget.group.name ?? "",
  maxLines: 1,
  style: style.labelSmall?.copyWith(fontSize: 13),
  overflow: TextOverflow.ellipsis,),
          ),

          const Spacer(flex: 2,),
          Text(widget.group.sendDate!.getFormattedTime(), maxLines: 1,
overflow: TextOverflow.ellipsis,style: style.labelSmall?.copyWith(fontSize: 10),),
        ],
      ),
      // subtitle: Text(friend.message ?? ""),
      subtitle: Row(
        children: [
          Expanded(
child:  chatMessage(isRead),
          ),

          if(myMessage())
Icon(widget.group.read == true || widget.group.delivered == true?
Icons.done_all_rounded : Icons.done_rounded,
  size: 16,
  color: widget.group.read == true? Colors.green.shade800:Colors.grey,),
          if (!isRead && !myMessage())
Consumer(builder: (context, ref, child) {

  final messageInfoAsync =ref.watch(getGroupMessageInfoProvider(widget.group,widget.userId));
  return messageInfoAsync.when(data: (data) {


      if(data?.read == true){
        return const SizedBox();
      }
      return const CircleAvatar(
        backgroundColor: Colors.orange,
        radius: 8,
        child: Text(
          '',
          style: TextStyle(
fontSize: 10,
          ),
        ),
      );

  }, error: (error, stackTrace) => const SizedBox(), loading: () =>  const SizedBox(width: 20, child: LinearProgressIndicator()));
},),
        ],
      ),
      onTap: () async {

AutoRouter.of(context).push(GroupMessageRoute(displayGroupName: widget.group.name ?? "",
myUserId:widget.userId,
friendUserId:widget.group.sendUserId ?? "",group: widget.group,profilePicture: widget.group.pictureStorageId));
        ref.read(groupMessagesInfoRepositoryProvider).updateGroupMemberMessagesInfoSeen(widget.group.id??"",widget.userId,widget.group.messageId?? "");


      },
    );
  }

  Widget chatMessage(bool isRead) {
    if (widget.group.messageType == 'IMAGE') {
      return const Align(
        alignment: Alignment.topLeft,
        child: Row(
          children: [
Icon(Icons.image,color: Colors.blue),
Padding(
  padding: EdgeInsets.only(left: 8.0),
  child: Text("image",style: TextStyle(color: Colors.blue,fontSize: 12),),
)
          ],
        ),
      );
    } else if (widget.group.messageType == 'VIDEO') {
      return const Align(
        alignment: Alignment.topLeft,
        child: Row(
          children: [
Icon(Icons.video_chat,color: Colors.blue,),
Padding(
  padding: EdgeInsets.only(left: 8.0),
  child: Text("video",style: TextStyle(color: Colors.blue,fontSize: 12),),
)
          ],
        ),
      );
    } else {
    return  Text(
        widget.group.message ?? "",
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
        style: TextStyle(
          fontSize: 12,
          fontWeight: !isRead && !myMessage() ? FontWeight.bold : null,
        ),
      );
    }
  }

  bool myMessage(){

    return widget.group.sendUserId == widget.userId;
  }


}
 

Code Snippet(UserImage.dart):



import 'dart:io';

import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/providers/FileTempProvider.dart';
import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../core/providers/GetUserProvider.dart';

class UserImage extends HookWidget{


  String userId;

  UserImage(this.userId, {super.key});

  @override
  Widget build(BuildContext context) {

    return SizedBox(
      width: 50,
      child: Consumer(builder: (context, ref, child) {
        final userAsync = ref.watch(getUserProvider((userId)));
        return userAsync.when(
          data: (user) => userDetail(user),
          error: (error, stackTrace) {
return defaultImage();
          },
          loading: () =>
          const SizedBox(width: 20, child: LinearProgressIndicator()),
        );
      },),
    );
  }

  CircleAvatar defaultImage() {
    return const CircleAvatar(
      radius: 20.0,
      backgroundImage: NetworkImage(Strings.avatarImageUrl),
    );
  }

  userDetail(UserAppwrite? user) {
    if(user != null){
      return Consumer(builder: (context, ref, child) {
        final fileAsync = ref.watch(fileTempProvider(Strings.profilePicturesBucketId,user.profilePictureStorageId??""));
        return fileAsync.when(
          data: (file) => memberProfilePicture(file,user),
          error: (error, stackTrace) {
return defaultImage();
          },
          loading: () =>
          const SizedBox(width: 20, child: LinearProgressIndicator()),
        );
      });
    }
    return  defaultImage();
  }

  memberProfilePicture(File file, UserAppwrite user) {

    Uint8List uint8list = Uint8List.fromList(file.readAsBytesSync());
    return Row(
      children: [
        CircleAvatar(
radius: 20,
backgroundImage: MemoryImage(uint8list)
        ),
      ],
    );
  }



}


 

Create Group:

Let's create groups. We are going to use MVC pattern

  • lib/pages/groups/create
  • CreateGroupViewModel.dart
  • CreateGroupScreen.dart - Screen to create a new chat group

Code Snippet(CreateGroupViewModel.dart):




import 'package:chat_with_bisky/core/providers/GroupRepositoryProvider.dart';
import 'package:chat_with_bisky/core/providers/RealmProvider.dart';
import 'package:chat_with_bisky/model/CreateGroupState.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupUserModel.dart';
import 'package:chat_with_bisky/model/db/ChatRealm.dart';
import 'package:chat_with_bisky/model/db/FriendContactRealm.dart';
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'CreateGroupViewModel.g.dart';
@riverpod
class CreateGroupViewModel extends _$CreateGroupViewModel{
  RealmRepositoryProvider get _realmRepositoryProvider => ref.read(realmRepositoryProvider);
  GroupRepositoryProvider get _groupRepositoryProvider => ref.read(groupRepositoryProvider);
  ValueChanged<bool>? onGroupCreated;
  @override
  CreateGroupState build(){

    return  CreateGroupState();
  }


  void userIdChanged(String userId) {

    state = state.copyWith(myUserId: userId);
  }


  void groupNameChanged(String groupName) {

    state = state.copyWith(group: GroupAppwrite(name: groupName));
  }


  void selectUserChanged(GroupUserModel userModel, bool isAdd) {

    List<GroupUserModel>? members = state.members;
    members ??= [];
    if(isAdd){
      members.add(userModel);
      state = state.copyWith(members: members);
    }else{
      state = state.copyWith(
          members:
          state.members?.where((e) => e.memberUserId != userModel.memberUserId).toList());
    }
  }

  getAllFriends(){

    List<ChatRealm> chats =  _realmRepositoryProvider.getMyChats(state.myUserId);
    List<GroupUserModel> allFriends = [];
    Set<String> mobileNumbers = {};
    if(chats.isNotEmpty){

      for (var element in chats) {

        if(!mobileNumbers.contains(element.senderUserId) && element.senderUserId!= state.myUserId){
          mobileNumbers.add(element.senderUserId??"");
          GroupUserModel user = GroupUserModel(memberUserId: element.senderUserId,base64Image: element.base64Image,name: element.displayName);
          allFriends.add(user);
        }
      }
    }

    List<FriendContactRealm> friends =  _realmRepositoryProvider.getMyFriends(state.myUserId);
    if(friends.isNotEmpty){

      for (var element in friends) {

        if(!mobileNumbers.contains(element.mobileNumber) && element.mobileNumber != state.myUserId){
          mobileNumbers.add(element.mobileNumber??"");
          GroupUserModel user = GroupUserModel(memberUserId: element.mobileNumber,base64Image: element.base64Image,name: element.displayName);
          allFriends.add(user);
        }
      }
    }
    state = state.copyWith(allFriends:allFriends);
  }


  createGroup() async{

    final group = GroupAppwrite(message: "New Group Created",name: state.group?.name,delivered: false,read: false,messageType: 'newGroup',createdDate: DateTime.now(),sendDate: DateTime.now());
    loader(true);
    bool success = await _groupRepositoryProvider.createGroup(group, state.members?? []) ?? false;
    loader(false);
    if(success == true){
      onGroupCreated!(true);
    }

  }

  loader(bool load){
    state = state.copyWith(loading:load);
  }

}


 

Code Snippet(CreateGroupScreen.dart):


import 'package:auto_route/annotations.dart';
import 'package:chat_with_bisky/model/CreateGroupState.dart';
import 'package:chat_with_bisky/model/GroupUserModel.dart';
import 'package:chat_with_bisky/pages/groups/create/CreateGroupViewModel.dart';
import 'package:chat_with_bisky/widget/LoadingPageOverlay.dart';
import 'package:chat_with_bisky/widget/friend_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

@RoutePage()
class CreateGroupScreen extends ConsumerStatefulWidget {
  final String myUserId;

  const CreateGroupScreen({super.key, required this.myUserId});

  @override
  ConsumerState<CreateGroupScreen> createState() => _CreateGroupScreenState();
}

class _CreateGroupScreenState extends ConsumerState<CreateGroupScreen> {
  final TextEditingController _groupNameController = TextEditingController();
  CreateGroupViewModel? notifier;
  CreateGroupState? state;

  _CreateGroupScreenState();

  @override
  Widget build(BuildContext context) {
    notifier = ref.read(createGroupViewModelProvider.notifier);
    state = ref.watch(createGroupViewModelProvider);
    notifier?.onGroupCreated = (value) {
      if(value == true){
        Navigator.pop(context);
      }
    };
    return LoadingPageOverlay(
      loading: state?.loading ?? false,
      child: Scaffold(
        appBar: AppBar(
backgroundColor: Colors.blue,
centerTitle: false,
title: const Text(
  'Select Members',
  style: TextStyle(color: Colors.white),
),
actions: <Widget>[
  state?.members?.isNotEmpty == true
      ? Padding(
          padding: const EdgeInsets.all(8.0),
          child: TextButton(
  onPressed: () {
    showGroupNameDialog();
  },
  child: const Text(
    "Create",
    style: TextStyle(color: Colors.white),
  )),
        )
      : const SizedBox()
]),
        body: ListView.builder(
itemCount: state?.allFriends?.length,
itemBuilder: (context, index) {
  GroupUserModel user = state?.allFriends?[index] ?? GroupUserModel();
  return Column(
    children: <Widget>[
      Padding(
        padding: const EdgeInsets.all(8),
        child: ListTile(
onTap: () {
  if (user.selected != true) {
    user.selected = true;
    notifier?.selectUserChanged(user, true);
  } else {
    user.selected = false;
    notifier?.selectUserChanged(user, false);
  }
},
leading: FriendImage(user.base64Image),
title: Text(
  '${user.name}',
  style: const TextStyle(
      color: Colors.black,
      fontWeight: FontWeight.bold,
      fontSize: 15),
),
trailing: user.selected == true
    ? const Icon(
        Icons.check_circle,
        color: Colors.black,
      )
    : const SizedBox()),
      ),
    ],
  );
}),
      ),
    );
  }

  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance
        .addPostFrameCallback((_) => initialization(context));
  }

  initialization(BuildContext context) {
    notifier?.userIdChanged(widget.myUserId);
    notifier?.getAllFriends();
  }

  showGroupNameDialog() {

    showDialog(
        context: context,
        builder: (context) {
          return Dialog(
  elevation: 14,
  child: SizedBox(
    height: 200,
    width: 370,
    child: Padding(
        padding: const EdgeInsets.only(
top: 38.0,
left: 16,
right: 16,
bottom: 16),
        child: Column(
          mainAxisSize: MainAxisSize.max,
          children: <Widget>[
TextField(
  controller: _groupNameController,
  decoration: InputDecoration(
    focusedBorder: const OutlineInputBorder(
        borderSide: BorderSide(
color: Colors.orange,
width: 2.0)),
    border: OutlineInputBorder(
        borderRadius:
        BorderRadius.circular(
0.0)),
    labelText: 'Group Name',
  ),
  textInputAction: TextInputAction.done,
  keyboardType: TextInputType.text,
  textCapitalization: TextCapitalization.sentences,
),
const Spacer(),
Row(
  children:  <Widget>[
    TextButton(
        onPressed: () {
          Navigator.pop(context);
        },
        child: const Text(
          'Cancel',
          style: TextStyle(
fontSize: 18,
          ),
        )),

    TextButton(
        onPressed:() {
          if(_groupNameController.text.isNotEmpty){
Navigator.pop(context);
notifier?.groupNameChanged(_groupNameController.text);
notifier?.createGroup();
          }

        },
        child: const Text('Create',
style: TextStyle(
    fontSize: 18,
    color: Colors.orange)
        )
    ),
  ],
)
          ],
        )),
  ));
        });
  }
}

 

Run the following command flutter packages pub run build_runner build

Code Snippet(class FriendListNotifier):


ref.keepAlive(); // add this in build()

 

Dashboard Group:

Let's list all groups on the dashboard tab and start group conversation

  • lib/pages/dashboard/groups
  • GroupsListViewModel.dart
  • GroupsListScreen.dart - Screen to list all members groups

Code Snippet(GroupsListViewModel.dart):




import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/providers/GroupRepositoryProvider.dart';
import 'package:chat_with_bisky/core/providers/RealtimeNotifierProvider.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/MyGroupsState.dart';
import 'package:chat_with_bisky/model/RealtimeNotifier.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'GroupsListViewModel.g.dart';
@riverpod
class GroupsListViewModel extends _$GroupsListViewModel{

  GroupRepositoryProvider get _groupRepositoryProvider => ref.read(groupRepositoryProvider);

  @override
  MyGroupsState build(){
    ref.keepAlive();
    _realtimeSynchronisation();
    return MyGroupsState();
  }

  setUserId(String userId){

    state = state.copyWith(myUserId: userId);
  }

  getMyGroups() async {
    List<GroupAppwrite> groups = await _groupRepositoryProvider.getMemberGroups(state.myUserId);

    if(state.groups == null){
      state = state.copyWith(groups: []);
    }
    state = state.copyWith(groups: groups);

  }

  void createOrUpdateChatHead(GroupAppwrite group,String type) {


    switch (type) {
      case RealtimeNotifier.create:
        state = state.copyWith(
groups: [
  group,
  ...state.groups??[]
]
        );

        break;

      case RealtimeNotifier.update:

        state =  state.copyWith(
groups:  state.groups?.map((e) => e.id == group.id ? group:e)
    .toList()
        );

        break;

      case RealtimeNotifier.delete:

        state =  state.copyWith(
groups:  state.groups?.where((e) => e.id != group.id)
    .toList()
        );


        break;

      default:
        break;
    }

  }


  _realtimeSynchronisation() {
    ref.listen<RealtimeNotifier?>(
        realtimeNotifierProvider.select((value) => value.asData?.value), (
        previous, next) async {
      if (next?.document.$collectionId == Strings.collectionGroupId) {
        final group = GroupAppwrite.fromJson(next!.document.data);
        final groupMember =await _groupRepositoryProvider.getGroupMemberByUserId(group.id??"", state.myUserId);
        if (groupMember != null) {
          createOrUpdateChatHead(group, next.type);

        }
      }
    });
  }

}


 

Code Snippet(GroupsListScreen.dart):


import 'package:auto_route/auto_route.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/MyGroupsState.dart';
import 'package:chat_with_bisky/pages/dashboard/groups/GroupsListViewModel.dart';
import 'package:chat_with_bisky/route/app_route/AppRouter.gr.dart';
import 'package:chat_with_bisky/service/LocalStorageService.dart';
import 'package:chat_with_bisky/widget/GroupTileItemWidget.dart';
import 'package:chat_with_bisky/widget/custom_app_bar.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:focus_detector/focus_detector.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

class GroupsListScreen extends  ConsumerStatefulWidget {
  @override
  ConsumerState<GroupsListScreen> createState() => _GroupsListScreenState();
}

class _GroupsListScreenState extends ConsumerState<GroupsListScreen> {


  GroupsListViewModel? _notifier;
  MyGroupsState? _state;
  @override
  Widget build(BuildContext context) {

    _notifier = ref.read(groupsListViewModelProvider.notifier);
    _state = ref.watch(groupsListViewModelProvider);

    return FocusDetector(
      onFocusGained: () {
        _notifier?.getMyGroups();
      },
      child: Scaffold(
        body: SingleChildScrollView(
          child: SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
  children: [

    CustomBarWidget(
      "Groups",
      actions: Row(
        children: [

IconButton(
  icon: const Icon(
    Icons.add,
    color: Colors.blue,
  ),
  onPressed: () async {

    AutoRouter.of(context).push(CreateGroupRoute( myUserId: _state?.myUserId??""));
  },
),

        ],
      ),
    ),

    _state?.groups?.isNotEmpty == true
        ? Expanded(
        child: ListView.separated(
padding: EdgeInsets.zero,
itemBuilder: (context, index) {
  GroupAppwrite group = _state!.groups![index];
  return GroupTileItemWidget(group: group,userId: _state?.myUserId?? "");
},
separatorBuilder: (context, index) {
  return const SizedBox(
    width: 1,
  );
},
itemCount: _state?.groups?.length ?? 0))
        : const Center(
      child: Text(
          "You are not in any group "),
    )


  ],
),
          ),
        ),
      ),
    );
  }



  Future<void> initialization() async {
    String userId =  await LocalStorageService.getString(LocalStorageService.userId) ?? "";
    _notifier?.setUserId(userId);
    _notifier?.getMyGroups();
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance
        .addPostFrameCallback((_) => initialization());

  }
}


 

Run the following command flutter packages pub run build_runner build

Group Details Screen:

Let's create screen and View Model for showing Group Details

  • lib/pages/dashboard/groups/details
  • GroupDetailsViewModel.dart
  • GroupDetailsScreen.dart - Screen to view group details such as members and exit from group

Code Snippet(GroupDetailsViewModel.dart):




import 'package:chat_with_bisky/core/providers/GroupRepositoryProvider.dart';
import 'package:chat_with_bisky/core/providers/ProfileRepositoryProvider.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupDetailsState.dart';
import 'package:chat_with_bisky/model/GroupMemberAppwrite.dart';
import 'package:flutter/services.dart';
import 'package:realm/realm.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'GroupDetailsViewModel.g.dart';

@riverpod
class GroupDetailsViewModel extends _$GroupDetailsViewModel{

  GroupRepositoryProvider get _groupRepositoryProvider => ref.read(groupRepositoryProvider);
  ProfileRepositoryProvider get _profileRepositoryProvider => ref.read(profileRepositoryProvider);

  @override
  GroupDetailsState build(){

    return GroupDetailsState();
  }

  setUserId(String userId){

    state = state.copyWith(myUserId: userId);
  }

  setGroup(GroupAppwrite group) async {
    state = state.copyWith(group: group,
        fileName:group.pictureStorageId);

  }
  getGroupMembers(String groupId) async {
    loader(true);
    List<GroupMemberAppwrite> members = await _groupRepositoryProvider.getGroupMembers(groupId);
    loader(false);
    if(members.isNotEmpty){
      setGroupMembers(members);
    }
  }

  setGroupMembers(List<GroupMemberAppwrite> members) async {
    state = state.copyWith(members: members);
  }

  loader(bool isLoading){
    state = state.copyWith(loading: isLoading);
  }


  getGroupImage() async {
    if(state.group?.pictureStorageId != null){
      Uint8List? image =await _profileRepositoryProvider.getExistingProfilePicture(state.group!.pictureStorageId!);
      if(image!= null){
        state = state.copyWith(image:image);
      }
    }
  }

  Future<bool> uploadGroupProfilePicture(String path) async {

    String id=ObjectId().hexString;
    bool isUploaded =await _profileRepositoryProvider.uploadGroupProfilePicture(state.fileName,id,path);
    if(isUploaded == true){
      await _groupRepositoryProvider.updateGroupProfilePicture(state.group!,id);
      GroupAppwrite? group =await _groupRepositoryProvider.getGroup(state.group?.id??"");
      if(group!=null){
        setGroup(group);
        getGroupImage();
        return true;
      }
    }else{
      // todo show error
    }

    return false;
  }





}


 

Code Snippet(GroupDetailsScreen.dart):


import 'dart:io';

import 'package:auto_route/auto_route.dart';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/extensions/extensions.dart';
import 'package:chat_with_bisky/core/providers/FileTempProvider.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupDetailsState.dart';
import 'package:chat_with_bisky/model/GroupMemberAppwrite.dart';
import 'package:chat_with_bisky/pages/dashboard/groups/details/GroupDetailsViewModel.dart';
import 'package:chat_with_bisky/widget/GroupMemberTileWidget.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';

@RoutePage()
class GroupDetailsScreen extends ConsumerStatefulWidget {
  final GroupAppwrite group;
  final String myUserId;

  const GroupDetailsScreen(
      {super.key, required this.group, required this.myUserId});

  @override
  ConsumerState<ConsumerStatefulWidget> createState() {
    return _GroupDetailsScreenState();
  }
}

class _GroupDetailsScreenState extends ConsumerState<GroupDetailsScreen> {
  GroupDetailsViewModel? _notifier;
  GroupDetailsState? _state;

  @override
  Widget build(BuildContext context) {
    _notifier = ref.read(groupDetailsViewModelProvider.notifier);
    _state = ref.watch(groupDetailsViewModelProvider);
    return Scaffold(
      appBar: AppBar(title: Text(widget.group.name ?? "")),
      body: SizedBox(
        width: MediaQuery.of(context).size.width, // added
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
GestureDetector(
  onTap: () => pickImage(ImageSource.gallery),
  child: Stack(
    alignment: Alignment.center,
    children: [
      Consumer(builder: (context, ref, child) {
        final fileAsync = ref.watch(fileTempProvider(
Strings.profilePicturesBucketId,
_state?.group?.pictureStorageId ?? ""));
        return fileAsync.when(
          data: (file) => groupPicturePicture(file),
          error: (error, stackTrace) {

return const CircleAvatar(
  radius: 50.0,
  backgroundImage: NetworkImage(Strings.avatarImageUrl),
);
          },
          loading: () => const SizedBox(
  width: 20, child: LinearProgressIndicator()),
        );
      }),
      const Icon(
        Icons.edit,
        color: Colors.white,
        size: 30,
      )
    ],
  ),
),
const Padding(
  padding: EdgeInsets.all(8.0),
  child: Text('Group members'),
),
if (_state?.members?.isNotEmpty == true)
  Expanded(
      child: ListView.separated(
          padding: EdgeInsets.zero,
          itemBuilder: (context, index) {
GroupMemberAppwrite member = _state!.members![index];
return GroupMemberTileWidget(
    member, _state?.myUserId ?? "");
          },
          separatorBuilder: (context, index) {
return const SizedBox(
  width: 1,
);
          },
          itemCount: _state?.members?.length ?? 0))
          ],
        ),
      ),
    );
  }

  groupPicturePicture(File file) {
    Uint8List uint8list = Uint8List.fromList(file.readAsBytesSync());
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        CircleAvatar(radius: 50.0, backgroundImage: MemoryImage(uint8list)),
      ],
    );
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => initialization());
  }

  Future<void> initialization() async {
    _notifier?.setUserId(widget.myUserId);
    _notifier?.setGroup(widget.group);
    _notifier?.getGroupMembers(widget.group.id ?? "");
    _notifier?.getGroupImage();
  }

  @override
  void setState(VoidCallback fn) {
    if (mounted) {
      super.setState(fn);
    }
  }

  void pickImage(ImageSource source) async {
    final file = await context.pickAndCropImage(3 / 4, source);

    if (file != null) {
      print(file.path);
      bool? fileUploaded =
          await _notifier?.uploadGroupProfilePicture(file.path);

      if (fileUploaded == true) {
        print('FILE UPLEADED');
      }
    }
  }
}
 

Run the following command flutter packages pub run build_runner build

Group Messages Screen:

Let's create Group Messages Screen

  • lib/pages/dashboard/groups/messages
  • GroupMessageViewModel.dart
  • GroupMessageScreen.dart - Screen to show messages and showing typing indicators, view you own messages information, send messagesm imagesm videos etc

Code Snippet(GroupMessageViewModel.dart):


import 'dart:async';

import 'package:appwrite/appwrite.dart';
import 'package:appwrite/models.dart';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/providers/DatabaseProvider.dart';
import 'package:chat_with_bisky/core/providers/FirebaseProvider.dart';
import 'package:chat_with_bisky/core/providers/GroupRepositoryProvider.dart';
import 'package:chat_with_bisky/core/providers/MessageRepositoryProvider.dart';
import 'package:chat_with_bisky/core/providers/RealmProvider.dart';
import 'package:chat_with_bisky/core/providers/RealtimeNotifierProvider.dart';
import 'package:chat_with_bisky/core/providers/StorageProvider.dart';
import 'package:chat_with_bisky/core/providers/UserRepositoryProvider.dart';
import 'package:chat_with_bisky/model/ChatAppwrite.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupMessageState.dart';
import 'package:chat_with_bisky/model/MessageAppwrite.dart';
import 'package:chat_with_bisky/model/MessageAppwriteDocument.dart';
import 'package:chat_with_bisky/model/MessageState.dart';
import 'package:chat_with_bisky/model/RealtimeNotifier.dart';
import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:chat_with_bisky/model/db/MessageRealm.dart';
import 'package:firebase_database/firebase_database.dart' as fd;
import 'package:flutter/foundation.dart';
import 'package:realm/realm.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:uuid/uuid.dart' as uuid;
import 'package:timeago/timeago.dart' as timeago;
import 'package:async/async.dart';
import 'package:chat_with_bisky/core/extensions/extensions.dart';
part 'GroupMessageViewModel.g.dart';


@riverpod
class GroupMessageNotifier extends _$GroupMessageNotifier {

  Databases get _databases => ref.read(databaseProvider);
  Storage get _storage => ref.read(storageProvider);
  MessageRepositoryProvider get _messageRepositoryProvider => ref.read(messageRepositoryProvider);
  GroupRepositoryProvider get _groupRepositoryProvider => ref.read(groupRepositoryProvider);
  UserDataRepositoryProvider get _userRepositoryProvider => ref.read(userRepositoryProvider);

  fd.FirebaseDatabase database = fd.FirebaseDatabase.instance;

  RestartableTimer? _timer;
  StreamSubscription? typingSubscription;

  @override
  GroupMessageState build() {
    _realtimeSynchronisation();
    return GroupMessageState();
  }

  void messageTypeChanged(String input) {
    state = state.copyWith(messageType: input);
  }

  void messageChanged(String input) {
    state = state.copyWith(message: input);
  }

  void myUserIdChanged(String input) {
    state = state.copyWith(myUserId: input);

     setUser(input);

  }

  Future<void> setUser(String input) async {
    if(state.user == null){
      UserAppwrite? user = await _userRepositoryProvider.getUser(input);
      if(user!=null){
        state = state.copyWith(user:user);
      }
    }
  }

  void updateGroupId(String groupId) {
    state = state.copyWith(groupId: groupId);
  }

  Future<void> sendMessage() async {
    try {
      MessageAppwrite message = MessageAppwrite();
      message.senderUserId = state.myUserId;
      message.receiverUserId = state.groupId;
      message.read = false;
      message.message = state.message;
      message.type = state.messageType;
      message.sendDate = DateTime.now();

      if (state.file != null) {
        message.fileName = state.file?.name;
      }

      Document document = await _databases.createDocument(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionMessagesId,
          documentId: const uuid.Uuid().v4(),
          data: message.toJson());

      state = state.copyWith(file: null);

      updateGroup(
          message,  state.groupId, state.myUserId, document.$id);

    } on AppwriteException catch (exception) {
      print(exception);
    }
  }

  Future<void> updateGroup(MessageAppwrite message,
      String groupId, String myUserId, String messageId) async {
    final group = GroupAppwrite(
        id: state.group?.id,
        createdDate: state.group?.createdDate,
        creatorUserId: state.group?.creatorUserId,
        pictureName: state.group?.pictureName,
        pictureStorageId: state.group?.pictureStorageId,
        description: state.group?.description,
        name: state.group?.name,
        delivered: false,
        read: false,
        messageType: message.type,
        messageId: messageId,
        sendUserId: myUserId,
        message: message.message,
        sendDate: DateTime.now());

    bool success = await _groupRepositoryProvider.updateGroup(group);
  }

  loader(bool load){
    state = state.copyWith(loading:load);
  }

  getDisplayName(String userId) async {
    try {
      Document document = await _databases.getDocument(
          databaseId: Strings.databaseId,
          collectionId: Strings.collectionUsersId,
          documentId: userId);

      UserAppwrite userAppwrite = UserAppwrite.fromJson(document.data);

      return userAppwrite.name;
    } on AppwriteException catch (exception) {
      print(exception);
    }

    return userId;
  }

  getMessages() async {

    List<MessageAppwriteDocument> messages = await _messageRepositoryProvider.getGroupMessages(state.groupId);

    if(messages.isNotEmpty){
      List<MessageRealm> realmMessages = [];
      initializeMessages();
      for (var element in messages) {

        final message = ref
.read(realmRepositoryProvider)
.mapMessageRealm(element.messageAppwrite!, element.document!);
        realmMessages.add(message);
      }
      state = state.copyWith(messages: realmMessages);
    }
  }

  saveMessage(
      MessageAppwrite messageAppwrite, Document document, String type) async {
    try {
      final message = ref
          .read(realmRepositoryProvider)
          .mapMessageRealm(messageAppwrite, document);

      initializeMessages();

      switch (type) {
        case RealtimeNotifier.create:
          state = state.copyWith(messages: [message, ...?state.messages]);

          updateMessageRead(message);

          break;

        case RealtimeNotifier.update:
          state = state.copyWith(
  messages: state.messages
      ?.map((e) => e.id == message.id ? message : e)
      .toList());

          updateMessageRead(message);
          break;

        case RealtimeNotifier.delete:
          state = state.copyWith(
  messages:
      state.messages?.where((e) => e.id != message.id).toList());

          break;

        default:
          break;
      }
    } catch (e) {
      print(e);
    }
  }

  void initializeMessages() {
     if(state.messages == null){
      state = state.copyWith(messages:[]);
    }
  }


  Future<Uint8List?> getFilePreview(String fileId) async {
    try {
      return await _storage.getFilePreview(
          bucketId: Strings.messagesBucketId, fileId: fileId);
    } catch (e) {
      print(e);
      return null;
    }
  }

  Future<File?> uploadMedia(String imageId, String filePath) async {
    try {
      state = state.copyWith(loading: true);
      File file = await _storage.createFile(
          bucketId: Strings.messagesBucketId,
          fileId: imageId,
          file: InputFile(
  path: filePath,
  filename: '$imageId.${getFileExtension(filePath)}'));

      state = state.copyWith(loading: false);

      return file;
    } catch (e) {
      state = state.copyWith(loading: false);
      print(e);
    }

    return null;
  }

  String getFileExtension(String filePath) {
    try {
      return '.${filePath.split('.').last}';
    } catch (e) {
      return "";
    }
  }

  _realtimeSynchronisation() {
    ref.listen<RealtimeNotifier?>(
        realtimeNotifierProvider.select((value) => value.asData?.value),
        (previous, next) {
      if (next?.document.$collectionId == Strings.collectionMessagesId) {
        final message = MessageAppwrite.fromJson(next!.document.data);
        if (message.receiverUserId == state.groupId) {
          saveMessage(message, next.document, next.type);
        }
      }
    });
  }

  void onChangedUploadedFile(File file) {
    state = state.copyWith(file: file);
  }

  updateMessageRead(MessageRealm message) {


  }

  myMessage(MessageRealm message) {
    return message.senderUserId == state.myUserId;
  }

  sendPushNotificationMessage(MessageAppwrite messageAppwrite) async {
    final friendUser = await ref
        .read(userRepositoryProvider)
        .getUser(messageAppwrite.receiverUserId ?? "");
    final myUser = await ref
        .read(userRepositoryProvider)
        .getUser(messageAppwrite.senderUserId ?? "");

    if (friendUser != null && myUser != null) {
      final body = {
        'fromName': myUser.name ?? "",
        'messageType': messageAppwrite.type ?? "",
        'message': messageAppwrite.message ?? "",
        'fromUserId': myUser.userId ?? "",
      };

      if (friendUser.firebaseToken != null) {
        sendPayload(friendUser.firebaseToken!, body);
      }
    }
  }




  Future<void> listenFriendIsTyping()async {


    final typingRef = database.ref().child('typing').child(state.groupId);
    typingRef.onValue.listen((event) {

      if(event.snapshot.exists){


        Map<Object?,Object?> map = event.snapshot.value as Map<Object?,Object?>;
        Map<Object?,Object?> typedValue = map[map.keys.first] as    Map<Object?,Object?>;
        Map<Object?,Object?> typedValueMap = typedValue[typedValue.keys.first] as    Map<Object?,Object?>;

        if(typedValueMap.containsKey('from') && typedValueMap['from'] != null ){
          String from = typedValueMap['from'] as String;

          if(from == state.myUserId){
state = state.copyWith(groupDetails: 'View Group Details');
return;
          }
        }
        if(typedValueMap.containsKey('memberName') && typedValueMap['memberName'] != null ){
          String memberName = typedValueMap['memberName'] as String;
          state = state.copyWith(groupDetails: '$memberName is typing....');
        }else{
          state = state.copyWith(groupDetails: 'typing....');
        }

      }else{
        state = state.copyWith(groupDetails: 'View Group Details');
      }

    });
  }

  Future<void> typingChanges(String text)async {

    if(_timer == null){

      fd.DatabaseReference con;
      await database.goOnline();
      final typingRef = ownUserTypingRef();
      await database.goOnline();
      con = typingRef.push();
      con.set({'from':state.myUserId,'to':state.groupId,'memberName':state.user?.name??''});
      resetOrStartTimer();
    }

  }

  fd.DatabaseReference ownUserTypingRef() {
    return database.ref().child('typing').child(state.groupId);
  }

  void resetOrStartTimer() {


    if(_timer == null){
      initializeTimer();
    }else{
      _timer?.reset();
    }

  }

  void initializeTimer() {
   _timer ??= RestartableTimer(const Duration(seconds: 5),  onTimerRunsOut);

  }



  onTimerRunsOut() {

    _timer = null;
    deleteTypingRef();

  }

  void deleteTypingRef() {

    final typingRef = ownUserTypingRef();
    typingRef.remove();
  }

  typingConnectionListener() async{

    final typingRef = ownUserTypingRef();
    await database.goOnline();
    typingSubscription = database.ref().child('.info/connected').onValue.listen((event) {

      if(event.snapshot.value != null){

        typingRef.onDisconnect().remove();
      }
    });

  }


  void disconnect(){

    if(typingSubscription != null){
      typingSubscription?.cancel();
    }

    database.goOffline();
    onTimerRunsOut();
  }

  setGroup(GroupAppwrite group) async {

      state = state.copyWith(group: group);
      state = state.copyWith(groupDetails: 'View Group Details');

  }

  Future<bool>deleteMessage(String messageId) async {
    loader(true);
    bool deleted  = await _messageRepositoryProvider.deleteMessage(messageId);
    loader(false);
    return deleted;
  }
}


 

Code Snippet(GroupMessageScreen.dart):


import 'package:appwrite/models.dart';
import 'package:auto_route/auto_route.dart';
import 'package:chat_bubbles/chat_bubbles.dart';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/extensions/extensions.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupMessageState.dart';
import 'package:chat_with_bisky/model/MessageState.dart';
import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:chat_with_bisky/model/db/MessageRealm.dart';
import 'package:chat_with_bisky/pages/dashboard/chat/MessageViewModel.dart';
import 'package:chat_with_bisky/pages/dashboard/chat/voice_calls/VoiceCallingPage.dart';
import 'package:chat_with_bisky/pages/dashboard/chat/voice_calls/VoiceCallsWebRTCHandler.dart';
import 'package:chat_with_bisky/pages/dashboard/groups/messages/GroupMessageViewModel.dart';
import 'package:chat_with_bisky/route/app_route/AppRouter.gr.dart';
import 'package:chat_with_bisky/widget/ChatHeadViewModel.dart';
import 'package:chat_with_bisky/widget/ChatMessageItem.dart';
import 'package:chat_with_bisky/widget/DefaultTempImage.dart';
import 'package:chat_with_bisky/widget/GroupChatMessageItem.dart';
import 'package:chat_with_bisky/widget/LoadingPageOverlay.dart';
import 'package:chat_with_bisky/widget/friend_image.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:realm/realm.dart' as realm;

@RoutePage()
class GroupMessageScreen extends ConsumerStatefulWidget {
  String displayGroupName;
  String myUserId;
  String friendUserId;
  GroupAppwrite group;
  String? profilePicture;

  GroupMessageScreen(
      {required this.displayGroupName,
      required this.myUserId,
      required this.friendUserId,
      required this.group,
      this.profilePicture});

  ConsumerState<GroupMessageScreen> createState() => _GroupMessageScreenState(
      displayName: displayGroupName, myUserId: myUserId, friendUserId: friendUserId
  ,profilePicture: profilePicture);
}

class _GroupMessageScreenState extends ConsumerState<GroupMessageScreen> {
  String displayName;
  String myUserId;
  String friendUserId;
  String? profilePicture;

  final TextEditingController _messageController = TextEditingController();
  GroupMessageNotifier? messageNotifier;
  GroupMessageState? messageState;
  final ScrollController _scrollController = ScrollController();

  _GroupMessageScreenState(
      {required this.displayName,
      required this.myUserId,
      required this.friendUserId,
        this.profilePicture});

  @override
  Widget build(BuildContext context) {
    messageNotifier = ref.read(groupMessageNotifierProvider.notifier);
    messageState = ref.watch(groupMessageNotifierProvider);
    return LoadingPageOverlay(
      loading: messageState?.loading ?? false,
      child: Scaffold(
        appBar: AppBar(
          elevation: 0,
          automaticallyImplyLeading: false,
          backgroundColor: Colors.white,
          flexibleSpace: SafeArea(
child: Container(
  padding: const EdgeInsets.only(right: 16),
  child: Row(
    children: <Widget>[
      IconButton(
        onPressed: () {
          Navigator.pop(context);
        },
        icon: const Icon(
          Icons.arrow_back,
          color: Colors.black,
        ),
      ),
      const SizedBox(
        width: 2,
      ),
      DefaultTempImage(Strings.profilePicturesBucketId,widget.group.pictureStorageId,size: 50,),
      const SizedBox(
        width: 12,
      ),
      Expanded(
        child: GestureDetector(
          onTap: () {
AutoRouter.of(context).push(GroupDetailsRoute(myUserId: messageState?.myUserId ??"", group: messageState?.group??GroupAppwrite()));
          },
          child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
  Text(
    displayName,
    style: const TextStyle(
        fontSize: 16, fontWeight: FontWeight.w600),
  ),
  const SizedBox(
    height: 6,
  ),
  Expanded(
    child: Text(
      messageState?.groupDetails ?? '',
      style: TextStyle(
          color:messageState?.groupDetails.contains('typing') == true?Colors.blue.shade600:Colors.grey.shade600, fontSize: 13)
    ),
  ),
],
          ),
        ),
      ),

    ],
  ),
),
          ),
        ),
        body: Form(
          child: Column(
children: [
  messageState?.messages?.isNotEmpty == true?
  Flexible(
      child: ListView.builder(
    controller: _scrollController,
    padding: const EdgeInsets.all(10.0),
    itemBuilder: (context, index) {
      MessageRealm message =
          messageState?.messages?.elementAt(index) ??
  MessageRealm(realm.ObjectId());

      return chatMessageItem(message);
    },
    itemCount: messageState?.messages?.length,
    reverse: true,
  )):Flexible(child: Container(),),
  chatInputWidget()
],
          ),
        ),
      ),
    );
  }

  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance
        .addPostFrameCallback((_) => initialization(context));
  }

  initialization(BuildContext context) {
    messageNotifier?.initializeMessages();
    messageNotifier?.listenFriendIsTyping();
    getMessages();
  }


  chatInputWidget() {


    return  MessageBar(
      onSend: (message) =>  sendMessage("TEXT", message),
      onTextChanged: (value) {
        messageNotifier?.typingChanges(value);
      },
      actions: [
        InkWell(
          child: const Icon(
Icons.add,
color: Colors.black,
size: 24,
          ),
          onTap: () {
_modalBottomSheet();
          },
        ),
        Padding(
          padding: EdgeInsets.only(left: 8, right: 8),
          child: InkWell(
child: Icon(
  Icons.camera_alt,
  color: Colors.blue,
  size: 24,
),
onTap: () {

  pickImage(ImageSource.camera);
},
          ),
        ),
      ],
    );
  }

  void pickImage(ImageSource source) async {
    Navigator.pop(context);
    final file = await context.pickAndCropImage(3 / 4, source);

    if (file != null) {
      print(file.path);

      File? fileUploaded = await messageNotifier?.uploadMedia(
          realm.ObjectId().hexString, file.path);

      if (fileUploaded != null) {
        sendMessage("IMAGE", fileUploaded.$id, file: fileUploaded);
      }
    }
  }

  void pickVideo() async {
    Navigator.pop(context);
    context.pickVideo(
      context,
      (file) async {
        print(file.path);

        File? fileUploaded = await messageNotifier?.uploadMedia(
realm.ObjectId().hexString, file.path);

        if (fileUploaded != null) {
          sendMessage("VIDEO", fileUploaded.$id, file: fileUploaded);
        }
      },
    );
  }

  void sendMessage(String type, String message, {File? file}) {
    if (file != null) {
      messageNotifier?.onChangedUploadedFile(file);
    }

    messageNotifier?.updateGroupId(widget.group.id??'');
    messageNotifier?.myUserIdChanged(myUserId);
    messageNotifier?.messageTypeChanged(type);
    messageNotifier?.messageChanged(message);
    messageNotifier?.sendMessage();
    _messageController.text = "";
  }

  void getMessages() {
    messageNotifier?.updateGroupId(widget.group.id??'');
    messageNotifier?.setGroup(widget.group);
    messageNotifier?.myUserIdChanged(myUserId);
    messageNotifier?.getMessages();
  }

  Widget chatMessageItem(MessageRealm documentSnapshot) {
    return GroupChatMessageItem(
      message: documentSnapshot,
      displayName: displayName,
      myMessage: documentSnapshot.senderUserId == myUserId,
      myUserId: myUserId,
      messageLongPress: (value) => onMessageLongPress(value),
    );
  }

  @override
  void setState(VoidCallback fn) {
    if (mounted) {
      super.setState(fn);
    }
  }

  void _modalBottomSheet() {
    showModalBottomSheet(
      context: context,
      builder: (builder) {
        return Container(
          height: 250.0,
          decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
    topLeft: Radius.circular(10), topRight: Radius.circular(10)),
          ),
          child: Container(
width: double.infinity,
color: Colors.white,
child: Column(
  children: [
    const SizedBox(
      height: 10,
    ),
    const Text("Select an Option"),
    const SizedBox(
      height: 10,
    ),
    InkWell(
      onTap: () {
        pickVideo();
      },
      child: Container(
        alignment: Alignment.center,
        width: double.infinity,
        height: 38,
        child: const Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
SizedBox(
  height: 10,
),
Icon(
  Icons.video_camera_front,
  color: Colors.grey,
),
Text("Video"),
Spacer()
          ],
        ),
      ),
    ),
    Divider(),
    InkWell(
      onTap: () {
        pickImage(ImageSource.gallery);
      },
      child: Container(
        alignment: Alignment.center,
        width: double.infinity,
        height: 38,
        child: const Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
SizedBox(
  height: 10,
),
Icon(
  Icons.image,
  color: Colors.grey,
),
Text("Image"),
Spacer()
          ],
        ),
      ),
    ),
    Divider(),
    InkWell(
      onTap: () {
        pickImage(ImageSource.camera);
      },
      child: Container(
        alignment: Alignment.center,
        width: double.infinity,
        height: 38,
        child: const Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
SizedBox(
  height: 10,
),
Icon(
  Icons.camera_alt,
  color: Colors.grey,
),
Text("Camera"),
Spacer()
          ],
        ),
      ),
    ),
  ],
),
          ),
        );
      },
    );
  }

  @override
  void dispose() {
    messageNotifier?.disconnect();
    ref.invalidate(chatHeadViewModelProvider);
    super.dispose();


  }


  void _messageInfoModalBottomSheet(MessageRealm messageRealm) {

    if(!myMessage(messageRealm)){
      return;
    }
    showModalBottomSheet(
      context: context,
      builder: (builder) {
        return Container(
          height: 250.0,
          decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
    topLeft: Radius.circular(10), topRight: Radius.circular(10)),
          ),
          child: Container(
width: double.infinity,
color: Colors.white,
child: Column(
  children: [
    const SizedBox(
      height: 10,
    ),
    const Text("Actions"),
    const SizedBox(
      height: 10,
    ),
    if(myMessage(messageRealm))
    InkWell(
      onTap: () {
        Navigator.pop(context);
        AutoRouter.of(context).push(GroupMessageDetailsRoute(myUserId: messageState?.myUserId ??"", group: messageState?.group??GroupAppwrite(), message: messageRealm));
      },
      child: Container(
        alignment: Alignment.center,
        width: double.infinity,
        height: 38,
        child: const Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
SizedBox(
  height: 10,
),
Icon(
  Icons.info,
  color: Colors.blue,
),
Padding(
  padding: EdgeInsets.all(8.0),
  child: Text("View Details"),
),
Spacer()
          ],
        ),
      ),
    ),
    const Divider(),
    if(myMessage(messageRealm))
    InkWell(
      onTap: () async{
        Navigator.pop(context);

        bool? deleted = await messageNotifier?.deleteMessage(messageRealm.messageIdUpstream??"");
      },
      child: Container(
        alignment: Alignment.center,
        width: double.infinity,
        height: 38,
        child: const Row(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
SizedBox(
  height: 10,
),
Icon(
  Icons.delete,
  color: Colors.red,
),
Padding(
  padding: EdgeInsets.all(8.0),
  child: Text("Delete"),
),
Spacer()
          ],
        ),
      ),
    ),

  ],
),
          ),
        );
      },
    );
  }

  bool myMessage(MessageRealm messageRealm) => myUserId == messageRealm.senderUserId;

  onMessageLongPress(MessageRealm value) {

    _messageInfoModalBottomSheet(value);
  }
}


 

Run the following command flutter packages pub run build_runner build

Group Message Deatils Screen:

Let's create Group Message Details Screen

  • lib/pages/dashboard/groups/messages/details
  • GroupMessageDetailViewModel.dart
  • GroupMessageDetailsScreen.dart- Screnn to show message information such as members delivered/read a message

Code Snippet(GroupMessageDetailViewModel.dart):




import 'package:chat_with_bisky/core/providers/GroupMessagesInfoRepositoryProvider.dart';
import 'package:chat_with_bisky/core/providers/GroupRepositoryProvider.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupMemberAppwrite.dart';
import 'package:chat_with_bisky/model/GroupMessageDetailsState.dart';
import 'package:chat_with_bisky/model/GroupMessageInfoAppwrite.dart';
import 'package:chat_with_bisky/model/db/MessageRealm.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'GroupMessageDetailViewModel.g.dart';

@riverpod
class GroupMessageDetailViewModel extends _$GroupMessageDetailViewModel{

  GroupRepositoryProvider get _groupRepositoryProvider => ref.read(groupRepositoryProvider);
  GroupMessagesInfoRepositoryProvider get _groupMessagesInfoRepositoryProvider => ref.read(groupMessagesInfoRepositoryProvider);

  @override
  GroupMessageDetailsState build(){

    return GroupMessageDetailsState();
  }

  setUserId(String userId){

    state = state.copyWith(myUserId: userId);
  }

  setGroup(GroupAppwrite group) async {
    state = state.copyWith(group: group);

  }
  setMessage(MessageRealm messageRealm) async {
    state = state.copyWith(message: messageRealm);

  }
  getGroupMembers(String groupId) async {
    List<GroupMemberAppwrite> members = await _groupRepositoryProvider.getGroupMembers(groupId);
    if(members.isNotEmpty){
      setGroupMembers(members);
    }
  }

  setGroupMembers(List<GroupMemberAppwrite> members) async {
    state = state.copyWith(members: members);
  }

  loader(bool isLoading){
    state = state.copyWith(loading: isLoading);
  }

  getMessageDeliveredMembers() async {

    List<GroupMessageInfoAppwrite> deliveredToMembers = await _groupMessagesInfoRepositoryProvider.getGroupMessageDelivered(state.message?.messageIdUpstream??"");
    state = state.copyWith(deliveredToMembers: deliveredToMembers);
  }

  getMessageReadsMembers() async {

    List<GroupMessageInfoAppwrite> readsToMembers = await _groupMessagesInfoRepositoryProvider.getGroupMessageReads(state.message?.messageIdUpstream??"");
    state = state.copyWith(readsToMembers: readsToMembers);
  }


}


 

Code Snippet(GroupMessageDetailsScreen.dart):


import 'dart:io';

import 'package:auto_route/auto_route.dart';
import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/extensions/extensions.dart';
import 'package:chat_with_bisky/core/providers/FileTempProvider.dart';
import 'package:chat_with_bisky/core/providers/GetUserProvider.dart';
import 'package:chat_with_bisky/model/AttachmentType.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';
import 'package:chat_with_bisky/model/GroupDetailsState.dart';
import 'package:chat_with_bisky/model/GroupMemberAppwrite.dart';
import 'package:chat_with_bisky/model/GroupMessageDetailsState.dart';
import 'package:chat_with_bisky/model/GroupMessageInfoAppwrite.dart';
import 'package:chat_with_bisky/model/db/MessageRealm.dart';
import 'package:chat_with_bisky/pages/dashboard/groups/details/GroupDetailsViewModel.dart';
import 'package:chat_with_bisky/pages/dashboard/groups/messages/details/GroupMessageDetailViewModel.dart';
import 'package:chat_with_bisky/widget/GroupMemberTileWidget.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';

@RoutePage()
class GroupMessageDetailsScreen extends ConsumerStatefulWidget {
  final GroupAppwrite group;
  final MessageRealm message;
  final String myUserId;

  const GroupMessageDetailsScreen(
      {super.key,
      required this.group,
      required this.myUserId,
      required this.message});

  @override
  ConsumerState<ConsumerStatefulWidget> createState() {
    return _GroupMessageDetailsScreenState();
  }
}

class _GroupMessageDetailsScreenState
    extends ConsumerState<GroupMessageDetailsScreen> {
  GroupMessageDetailViewModel? _notifier;
  GroupMessageDetailsState? _state;

  @override
  Widget build(BuildContext context) {
    _notifier = ref.read(groupMessageDetailViewModelProvider.notifier);
    _state = ref.watch(groupMessageDetailViewModelProvider);
    return Scaffold(
      appBar: AppBar(title: Text(widget.group.name ?? "")),
      body: SizedBox(
        width: MediaQuery.of(context).size.width, // added
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
messageDetails(),
const Padding(
  padding: EdgeInsets.all(8.0),
  child: Text('Read By:',style: TextStyle(fontWeight: FontWeight.bold,color: Colors.blue)),
),
if (_state?.readsToMembers?.isNotEmpty == true)
  Expanded(
      child: ListView.separated(
          shrinkWrap: true,
          padding: EdgeInsets.zero,
          itemBuilder: (context, index) {
GroupMessageInfoAppwrite groupMessageInfoAppwrite =
    _state!.readsToMembers![index];
return memberWidget(groupMessageInfoAppwrite);
          },
          separatorBuilder: (context, index) {
return const SizedBox(
  width: 1,
);
          },
          itemCount: _state?.readsToMembers?.length ?? 0)),
const Padding(
  padding: EdgeInsets.all(8.0),
  child: Text('Delivered To:',style: TextStyle(fontWeight: FontWeight.bold,color: Colors.orange)),
),
if (_state?.deliveredToMembers?.isNotEmpty == true)
  Expanded(
      child: ListView.separated(
          padding: EdgeInsets.zero,
    shrinkWrap: true,
          itemBuilder: (context, index) {
GroupMessageInfoAppwrite groupMessageInfoAppwrite =
    _state!.deliveredToMembers![index];
return memberWidget(groupMessageInfoAppwrite);
          },
          separatorBuilder: (context, index) {
return const SizedBox(
  width: 1,
);
          },
          itemCount: _state?.deliveredToMembers?.length ?? 0))
          ],
        ),
      ),
    );
  }

  messageDetails() {
    return SizedBox(
      child: Column(
        children: [
          Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
  children: [
    const Padding(
      padding: EdgeInsets.all(8.0),
      child: Text('Type:',style: TextStyle(fontWeight: FontWeight.bold)),
    ),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: Text(
          AttachmentType.messageType(_state?.message?.type ?? '')),
    ),
  ],
),
          ),
          Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
  children: [
    const Padding(
      padding: EdgeInsets.all(8.0),
      child: Text('Message:',style: TextStyle(fontWeight: FontWeight.bold),),
    ),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: getMessage(_state?.message),
    ),
  ],
),
          ),
        ],
      ),
    );
  }

  memberWidget(GroupMessageInfoAppwrite groupMessageInfoAppwrite) {
    return Consumer(
      builder: (context, ref, child) {
        final userAsync = ref
.read(GetUserProvider(groupMessageInfoAppwrite.memberUserId ?? ""));

        return userAsync.when(
          data: (user) {
if (user != null) {
  return GroupMemberTileWidget(
      GroupMemberAppwrite(
          name: user.name, memberUserId: user.userId),
      _state?.myUserId ?? "");
}

return const SizedBox();
          },
          error: (error, stackTrace) {
print(stackTrace);
return const SizedBox();
          },
          loading: () =>
  const SizedBox(width: 20, child: LinearProgressIndicator()),
        );
      },
    );
  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => initialization());
  }

  Future<void> initialization() async {
    _notifier?.setUserId(widget.myUserId);
    _notifier?.setGroup(widget.group);
    _notifier?.setMessage(widget.message);
    _notifier?.getGroupMembers(widget.group.id ?? "");
    _notifier?.getMessageDeliveredMembers();
    _notifier?.getMessageReadsMembers();
  }

  @override
  void setState(VoidCallback fn) {
    if (mounted) {
      super.setState(fn);
    }
  }

  getMessage(MessageRealm? message) {
    if (message == null) {
      return const SizedBox();
    }
    if (AttachmentType.text == message.type) {
      return Text(
        message.message ?? "",
        overflow: TextOverflow.ellipsis,
      );
    } else if (AttachmentType.image == message.type) {
      return const Icon(
        Icons.image,
        color: Colors.grey,
      );
    } else if (AttachmentType.video == message.type) {
      return const Icon(
        Icons.video_call,
        color: Colors.grey,
      );
    } else if (AttachmentType.voice == message.type) {
      return const Icon(
        Icons.keyboard_voice,
        color: Colors.grey,
      );
    }
    return const SizedBox();
  }
}


 

Run the following command flutter packages pub run build_runner build

Add Group BottomNavigationBarItem on dashboard:

Let's add group BottomNavigationBarItem

  • lib/pages/dashboard
  • DashboardPage.dart

Run the following command flutter packages pub run build_runner build

Code Snippet(lib/route/app_route/AppRouter.dart):

Add below routes in AppRouter class. These routes are for the screen we created


    AutoRoute(page: CreateGroupRoute.page),
    AutoRoute(page: GroupMessageRoute.page),
    AutoRoute(page: GroupDetailsRoute.page),
    AutoRoute(page: GroupMessageDetailsRoute.page),
 

Code Snippet(DashboardPage.dart):


import 'package:chat_with_bisky/pages/dashboard/groups/GroupsListScreen.dart';
// add group BottomNavigationBarItem
BottomNavigationBarItem(
  icon: Image.asset(
    "assets/images/friends.png",
    width: 20,
    height: 20,
  ),
  label: "Groups",
  activeIcon: Image.asset(
    "assets/images/friends.png",
    width: 20,
    height: 20,
    color: Colors.blue,
  )),
// change case 1 to return GroupsListScreen() and case 2 to return FriendsListScreen()

  Widget getSelectedScreen(int index) {
    switch (index) {
      case 0:
        return ChatListScreen();
      case 1:
        return GroupsListScreen();
      case 2:
        return FriendsListScreen();

      default:
         return Container();
    }
  }
 

Code Snippet(main.dart):

Let's implement delivered group messages in main.dart background service


import 'package:chat_with_bisky/core/providers/GroupMessagesInfoRepositoryProvider.dart';
import 'package:chat_with_bisky/core/providers/GroupRepositoryProvider.dart';
import 'package:chat_with_bisky/model/GroupAppwrite.dart';

// add below code to handle group messages delivered in _realtimeSynchronisation

        if (next?.document.$collectionId == Strings.collectionGroupId) {
      final group = GroupAppwrite.fromJson(next!.document.data);

      if (userId != null) {

       final member = await _containerGlobal
.read(groupRepositoryProvider).getGroupMemberByUserId(group.id??"", userId);


        if (member != null && group.sendUserId != userId) {

          _containerGlobal
  .read(groupMessagesInfoRepositoryProvider)
  .updateGroupMemberMessagesInfoDelivered(group,userId,group.messageId??"");
        }
      }
    }
 

Code Snippet(app/src/main/AndroidManifest.xml.dart):

Add below permissions so that the application will run on android without any issue. Add cropping activity, we using it to crop group profile picture


//    add below permission
    
    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
// add cropping activity in application tag
    
    <activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>



 

Conclusion:

Congratulations on completing this in-depth tutorial on creating an advanced chat group app with Flutter and Appwrite! You've now equipped yourself with the knowledge to build a powerful and interactive messaging platform that encompasses a range of essential features, from deleting messages to tracking read and seen indicators.

Through hands-on coding and step-by-step guidance, you've learned how to integrate Appwrite's backend capabilities seamlessly with Flutter's UI prowess. By implementing features like updating group pictures, displaying message information, and indicating typing status, you're well on your way to creating user experiences that stand out.

Remember, the journey of learning and coding is an ongoing one. Feel free to explore and experiment with these concepts further, adding your unique touch to enhance the functionality and visual appeal of your apps.

Thank you for joining us on this exciting coding adventure! If you found this blog valuable, don't forget to share, and subscribe to our YouTube channel for more engaging Flutter content. Keep coding, keep innovating, and keep pushing the boundaries of what you can create.