skip to Main Content

I am Trying to make a ChatApp using Flutter’s dart_pusher_channels and Laravel Reverb. AS given in Laravel docs I have setup Sanctum authentication. Then created an Event in Laravel:

class MessageSent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     */
    public $message;
    public function __construct(Messages $message)
    {
        $this->message = $message;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array<int, IlluminateBroadcastingChannel>
     */
    public function broadcastOn(): array
    {
        $channels = [];

        $participants = ChatParticipants::where('chat_id', $this->message->chat_id)->pluck('user_id');

        foreach ($participants as $userId) {
            $channels[] = new PrivateChannel('chat.' . $userId);
        }
        return $channels;
    }
    public function broadcastAs(): string
    {
        return 'message.sent';
    }
}

Then I have updated the routes/channels.php:

Broadcast::channel('chat.{id}', function (User $user, $id) {
    return $user->id === $id;
});

I have also tried with ShouldBroadcastNow…Next I have made changes in bootstrap/app.php

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        // channels: __DIR__.'/../routes/channels.php',
        health: '/up',
    )
    ->withBroadcasting(
        channels: __DIR__.'/../routes/channels.php',
        attributes: ['prefix' => 'api', 'middleware' => ['auth:sanctum']],
    )
    ->withMiddleware(function (Middleware $middleware) {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

then after message is created in the db I have tried to broadcast the message :
broadcast(new MessageSent($message));
Then I ran my backend server on port 8000,also my reverb server using ‘php artisan reverb:start –debug’ on port 8080 and also ‘php artisan queue:list’.Now in flutter I have used this snippet to connect to server :

 Future<void> dartconnect() async {
    try {
      final auth = Provider.of<AuthState>(context, listen: false);

      const hostOptions = PusherChannelsOptions.fromHost(
        scheme: 'ws',
        host: '*.*.*.*',
        key: 'reverb_key',
        shouldSupplyMetadataQueries: true,
        metadata: PusherChannelsOptionsMetadata.byDefault(),
        port: 8080,
      );

      final client = PusherChannelsClient.websocket(
          options: hostOptions,
          connectionErrorHandler: (exception, trace, refresh) {
            refresh();
          });

      final private = client.privateChannel(
        'chat.${auth.user!.id}',
        authorizationDelegate:
            EndpointAuthorizableChannelTokenAuthorizationDelegate
                .forPrivateChannel(
          authorizationEndpoint: Uri.parse('$domain/api/broadcasting/auth'),
          headers: {
            'Authorization': 'Bearer ${auth.loginToken}',
          },
        ),
      );

      StreamSubscription<ChannelReadEvent> privateSubscription =
          private.bind('message.sent').listen((event) {
        print('Private event received:${event.data} ');
      });

      client.onConnectionEstablished.listen((s) {
        print('Connection established');
        private.subscribe();
      });

      await client.connect();
  
    } catch (e) {
      
    }
  }

I am not able to get response in my frontend. I have tried other drivers like pusher as well and used other flutter packages like Web socket channel, pusher channels fixed, etc. But none seems to work

2

Answers


  1. create a

    pusher_connect.dart

        import 'dart:async';
        
        import 'package:dart_pusher_channels/dart_pusher_channels.dart';
        import 'package:get/get.dart';
        
        import '../controllers/auth/auth_controller.dart';
    
        final RxString socketId = ''.obs;
    
        Future<void> connect(String id) async {
          const hostOptions = PusherChannelsOptions.fromHost(
            scheme: 'ws',
            host: '192.168.122.1',
            key: 'reverb app key',
            shouldSupplyMetadataQueries: true,
            metadata: PusherChannelsOptionsMetadata.byDefault(),
            port: 6001,
          );
        
          final client = PusherChannelsClient.websocket(
              options: hostOptions,
              connectionErrorHandler: (exception, trace, refresh) {
                refresh();
              });
        
          final channel = client.publicChannel(
            'public-user.5.wallet',
          );
        
          final chatsChannel = client.privateChannel(
            'private-chats.$id',
            authorizationDelegate:
                EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel(
              authorizationEndpoint: Uri.parse('your-domain/api/broadcasting/auth'),
              headers: {
                'Authorization': 'Bearer $token',
              },
            ),
          );
        
          StreamSubscription<ChannelReadEvent> channelSubscription =
              channel.bind('App\Events\MessageCreated').listen((event) {
            print('Event received: ${event.data}');
          });
        
          StreamSubscription<ChannelReadEvent> privateSubscription =
              chatsChannel.bind('App\Events\MessageCreated').listen((event) {
            print('Private event received: ${event.data}');
          });
        
          client.onConnectionEstablished.listen((s) {
            print('Connection established');
            channel.subscribe();
            chatsChannel.subscribe(); 
    socketId.value = chatsChannel.connectionDelegate.socketId!;
          });
        
          await client.connect();
        }
    

    in your php event file, It is best to hook the broadcast event to a parent model id of the ChatParticipants,in this case Chat model.

    public function broadcastOn(): array
        {
            return [
                new PrivateChannel('chat.'. $this->message->chat_id),
            ];
        }
    

    routes/channels.php:

        Broadcast::channel('chat.{id}', function (User $user, $id) {
            $chat = Chat::find($id);
    return Gate::authorize('view', $chat);
        });
    

    make sure to add ‘X-Socket-ID’: socketId.value to your headers when submiting data to your server.

    headers = {
            'Accept': 'application/json',
            'Authorization': 'Bearer $token',
            'X-Socket-ID': socketId.value
          };
    
          var request = http.MultipartRequest('POST', url);
          request.headers.addAll(headers);
    
    Login or Signup to reply.
  2. in CommentsController file

    class CommentsController extends Controller
                {
                    public function __construct()
                    {
                        $this->middleware('auth:sanctum');
                    }
                public function store(Request $request): IlluminateHttpJsonResponse
                    {
                        $this->validate($request, [
                            'message' => 'required|string|max:255',
                        ]);
            $hostel = Hostel::findHostel($request->hostelName,withoutEagerLoads:true);
                        /** @var Comment $comment */
                        $comment = $request->user()->comments()->create([
                            'message' => $request->message,
                            'commentable_type' => Hostel::class,
                            'commentable_id' => $hostel->id,
                        ]);
        if ($request->hasFile('images')){
                    foreach ($request->file('images') as $image) {
                        try {
                            $comment->addMedia($image)->toMediaCollection('images');
                        } catch (FileDoesNotExist|FileIsTooBig $e) {
                            report($e);
                        }
                    }
                }
        
                // Broadcast the event
                broadcast(new CommentCreatedEvent($comment))->toOthers();
        
                return response()->json(new CommentResource($comment->fresh([
                    'media',
        //            'commentReply'=>['client.media','media','votes'],
                    'client.media',
                    'votes'])),201);
            }
    

    // in CommentCreatedEvent file

    class CommentCreatedEvent implements ShouldBroadcastNow
    {
        use Dispatchable, InteractsWithSockets, SerializesModels;
    
        public Comment $comment;
        /**
         * Create a new event instance.
         */
        public function __construct(Comment $comment)
        {
            $this->comment = $comment;
        }
    
        /**
         * Get the channels the event should broadcast on.
         *
         * @return PrivateChannel
         */
        public function broadcastOn(): array
        {
            return [new PrivateChannel('comments.'. $this->comment->commentable_id)];
        }
    
    
           /**
            * Get the data to broadcast.
            *
            * @return array
            */
           public function broadcastWith(): array
           {
               $resource = CommentResource::make($this->comment->load([
                       'media',
                        'commentReply'=>['client.media','media','votes'],
                        'client.media',
                        'votes'
                   ]), true);
               return [
                   'comment' =>  $resource,
               ];
           }
    }
    

    in channels.php file

        Broadcast::channel('comments.{commentId}', function(AppModelsUser $user,string $hostelName){
        return AppModelsAccommodationHostelHostel::findHostel($hostelName,withoutEagerLoads:true)->exists();
    });
    

    in hostel_socket.dart file

    class HostelSocket{
      String hostelName;
      HostelSocket({required this.hostelName});
      
      final RxString socketId = ''.obs;
      late final PusherChannelsClient client;
      late final StreamSubscription<ChannelReadEvent> commentSubscription;
      
      late final PrivateChannel commentChannel;
    
      Future<void> connectToHostelChannel() async {
        const hostOptions = PusherChannelsOptions.fromHost(
        scheme: 'ws',
        host: '192.168.122.1',
        key: 'reverb app key',
        shouldSupplyMetadataQueries: true,
        metadata: PusherChannelsOptionsMetadata.byDefault(),
        port: 6001,
      );
        client = PusherChannelsClient.websocket(
            options: hostOptions,
            connectionErrorHandler: (exception, trace, refresh) {
              refresh();
            });
    
        
        
    
        commentChannel = client.privateChannel(
          'private-comments.$hostelName',
          authorizationDelegate:
          EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel(
            authorizationEndpoint: Uri.parse('$broadcastingAuth?hostelName=$hostelName'),
            headers: {
              'Authorization': 'Bearer ${Get.find<AuthController>().box.read('token')}',
            },
          ),
        );
    
       
    
        commentSubscription = commentChannel.bind('App\Events\CommentCreatedEvent').listen((event) {
          final eventData = jsonDecode(event.data);
          final newComment = Comment.fromJson(eventData['comment']); // Assuming the event data contains a 'comment' key
    
          
          if (Get.isRegistered<HostelController>()) {
            var commentList = Get.find<HostelController>().commentList;
    
            // Check if the comment already exists in the list
            if (!commentList.value.data.any((existingComment) => existingComment.id == newComment.id)) {
              // Add the new comment to the list
              commentList.value.data.add(newComment);
            } else {
              // Update the existing comment in the list
              final index = commentList.value.data.indexWhere((existingComment) => existingComment.id == newComment.id);
              if (index != -1) {
                commentList.value.data[index] = newComment;
              }
            }
            // Update the UI
            Get.find<HostelController>().newMessageCount++;
            Get.find<HostelController>().update();
          }
        });
    
        client.onConnectionEstablished.listen((_) {
         
          commentChannel.subscribe();
          socketId.value = commentChannel.connectionDelegate.socketId!;
          // socketId.value = reservationsChannel.connectionDelegate.socketId!;
        });
    
        await client.connect();
      }
    
    
      void disconnect(){
        
        commentSubscription.cancel();
        
        commentChannel.unsubscribe();
        
        client.disconnect();
      }
    }
    

    in hostelController.dart file

        class HostelController extends GetxController{
    
    final box = GetStorage();
      var commentList = CommentsCollectionModel(
              data: <Comment>[].obs,
              links: Links(first: '', last: '', prev: null, next: ''),
              meta: Meta(currentPage: 0, from: 0, lastPage: 0, links: [], path: '', perPage: 0, to: 0, total: 0))
          .obs;
    
      var _socketId = ''.obs;
    
      get socketId => _socketId.value;
    
      set setSocketId(String newState) {
        _socketId.value = newState;
      }
          @override
          void onInit() {
            super.onInit();
            hostelName = Get.arguments['hostelName'];
            fetchHostel().then((_){
              if(hostel != null){
                hostelSocket = HostelSocket(hostelName: hostel!.id);
                hostelSocket!.connectToHostelChannel();
               setSocketId = hostelSocket!.socketId.value;
              }
            });
          }
     
    
         @override
          void onClose() {
            if(hostel != null){
              hostelSocket!.disconnect();
            }
            super.onClose();
          }
    Future<void> sendCommentToServer(String content) async {
        try {
          setCommentLoading = true;
          var hostelName = Get.find<RootController>().selectedHostel?.id;
          final url = Uri.parse('$appBaseUrl$hostelName/comments');
          String? token = box.read("hotelierToken");
          Map<String, String> headers = {
            'Accept': 'application/json',
            'Authorization': 'Bearer $token',
            'X-Socket-ID': socketId
          };
          var request = http.MultipartRequest('POST', url);
          request.headers.addAll(headers);
          request.fields['message'] = content;
    
          var mediaIndex = 0;
          for (var medium in Get.find<CommentInputController>().selectedMedia) {
            final file = await medium.getFile();
            request.files.add(await http.MultipartFile.fromPath(
              'images[$mediaIndex]', // Use an array field name for multiple files
              file.path,
            ));
            mediaIndex++;
          }
          var response = await request.send();
    
          if (response.statusCode == 201) {
            // Comment successfully sent, add it to the list
            var responseStream = response.stream;
            var responseBody = await responseStream.transform(utf8.decoder).join();
    
            Comment newComment = Comment.fromJson(json.decode(responseBody));
            addComment(newComment);
          } else {
            // Handle error (e.g., show a snackbar)
            Get.snackbar('Error', 'Failed to send comment');
          }
        } catch (e) {
          debugPrint(e.toString());
          // Handle network or other errors
          Get.snackbar('Error', 'Failed to send comment catch');
        } finally {
          setCommentLoading = false;
          Get.find<CommentInputController>().selectedMedia.clear();
          update();
        }
      }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search