dart-lang / web_socket_channel

StreamChannel wrappers for WebSockets.
https://pub.dev/packages/web_socket_channel
BSD 3-Clause "New" or "Revised" License
412 stars 107 forks source link

Web version websocket doesn't work, but ios does #328

Closed Kirimatt closed 4 months ago

Kirimatt commented 4 months ago

Hey!

I'm pretty new in Flutter and I have simple project for chat using web_socket_channel.

The problem is that WebSocketChannel works properly on ios emulator, but it doesn't in web chrome.

It seems that web application always trying to establish connect and I see the first message on backend. However, there is nothing on web page.

Also I have backend on golang and using gorilla/websocket for server.

I hope it's mine misleading. There is main.dart code for my application

import 'dart:convert';
import 'dart:io';

import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:mime/mime.dart';
import 'package:open_filex/open_filex.dart';
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';
import 'package:web_socket_channel/status.dart' as status;
import 'package:web_socket_channel/web_socket_channel.dart';

void main() {
  final uuid = const Uuid().v4().toString();
  final webSocketChannel = WebSocketChannel.connect(
    Uri.parse('ws://localhost:8080'),
    protocols: [uuid],
  );
  final me = types.User(id: uuid, firstName: 'User');

  initializeDateFormatting().then((_) => runApp(MyApp(
    webSocketChannel,
    uuid,
    me,
  )));
}

class MyApp extends StatelessWidget {
  final WebSocketChannel webSocketChannel;
  final String uuid;
  final types.User me;
  const MyApp(
      this.webSocketChannel,
      this.uuid,
      this.me,
      {super.key,}
      );

  @override
  Widget build(BuildContext context) => MaterialApp(
        home: ChatPage(
          webSocketChannel,
          uuid,
          me,
        ),
      );
}

class ChatPage extends StatefulWidget {
  final WebSocketChannel webSocketChannel;
  final String uuid;
  final types.User me;

  const ChatPage(
      this.webSocketChannel,
      this.uuid,
      this.me,
      {super.key,}
  );

  @override
  State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
  List<types.Message> _messages = [];

  @override
  void initState() {
    widget.webSocketChannel.stream.listen((incomingMessage) {
      final Map<String, dynamic> data = jsonDecode(incomingMessage);
      onMessageReceived(types.Message.fromJson(data));
    }, onDone: () {
      print('WebSocket connection closed');
    }, onError: (error) {
      print('WebSocket error: $error');
    }, cancelOnError: true,
    );
    super.initState();
    _loadMessages();
  }

  void _addMessage(types.Message message) {
    setState(() {
      _messages.insert(0, message);
    });
  }

  void _handleAttachmentPressed() {
    showModalBottomSheet<void>(
      context: context,
      builder: (BuildContext context) => SafeArea(
        child: SizedBox(
          height: 144,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              TextButton(
                onPressed: () {
                  Navigator.pop(context);
                  _handleImageSelection();
                },
                child: const Align(
                  alignment: AlignmentDirectional.centerStart,
                  child: Text('Photo'),
                ),
              ),
              TextButton(
                onPressed: () {
                  Navigator.pop(context);
                  _handleFileSelection();
                },
                child: const Align(
                  alignment: AlignmentDirectional.centerStart,
                  child: Text('File'),
                ),
              ),
              TextButton(
                onPressed: () => Navigator.pop(context),
                child: const Align(
                  alignment: AlignmentDirectional.centerStart,
                  child: Text('Cancel'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void _handleFileSelection() async {
    final result = await FilePicker.platform.pickFiles(
      type: FileType.any,
    );

    if (result != null && result.files.single.path != null) {
      final message = types.FileMessage(
        author: widget.me,
        createdAt: DateTime.now().millisecondsSinceEpoch,
        id: const Uuid().v4(),
        mimeType: lookupMimeType(result.files.single.path!),
        name: result.files.single.name,
        size: result.files.single.size,
        uri: result.files.single.path!,
      );

      _addMessage(message);
    }
  }

  void _handleImageSelection() async {
    final result = await ImagePicker().pickImage(
      imageQuality: 70,
      maxWidth: 1440,
      source: ImageSource.gallery,
    );

    if (result != null) {
      final bytes = await result.readAsBytes();
      final image = await decodeImageFromList(bytes);

      final message = types.ImageMessage(
        author: widget.me,
        createdAt: DateTime.now().millisecondsSinceEpoch,
        height: image.height.toDouble(),
        id: const Uuid().v4(),
        name: result.name,
        size: bytes.length,
        uri: result.path,
        width: image.width.toDouble(),
      );

      _addMessage(message);
    }
  }

  void _handleMessageTap(BuildContext _, types.Message message) async {
    if (message is types.FileMessage) {
      var localPath = message.uri;

      if (message.uri.startsWith('http')) {
        try {
          final index =
              _messages.indexWhere((element) => element.id == message.id);
          final updatedMessage =
              (_messages[index] as types.FileMessage).copyWith(
            isLoading: true,
          );

          setState(() {
            _messages[index] = updatedMessage;
          });

          final client = http.Client();
          final request = await client.get(Uri.parse(message.uri));
          final bytes = request.bodyBytes;
          final documentsDir = (await getApplicationDocumentsDirectory()).path;
          localPath = '$documentsDir/${message.name}';

          if (!File(localPath).existsSync()) {
            final file = File(localPath);
            await file.writeAsBytes(bytes);
          }
        } finally {
          final index =
              _messages.indexWhere((element) => element.id == message.id);
          final updatedMessage =
              (_messages[index] as types.FileMessage).copyWith(
            isLoading: null,
          );

          setState(() {
            _messages[index] = updatedMessage;
          });
        }
      }

      await OpenFilex.open(localPath);
    }
  }

  void _handlePreviewDataFetched(
    types.TextMessage message,
    types.PreviewData previewData,
  ) {
    final index = _messages.indexWhere((element) => element.id == message.id);
    final updatedMessage = (_messages[index] as types.TextMessage).copyWith(
      previewData: previewData,
    );

    setState(() {
      _messages[index] = updatedMessage;
    });
  }

  void _handleSendPressed(types.PartialText message) {
    final textMessage = types.TextMessage(
      author: widget.me,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: const Uuid().v4(),
      text: message.text,
      type: types.MessageType.text,
      roomId: widget.uuid,
      status: types.Status.seen,
    );

    if (textMessage.text.isNotEmpty) {
      widget.webSocketChannel.sink.add(json.encode(textMessage));
      _addMessage(textMessage);
    }
  }

  void _loadMessages() async {
    final response = await rootBundle.loadString('assets/messages.json');
    final messages = (jsonDecode(response) as List)
        .map((e) => types.Message.fromJson(e as Map<String, dynamic>))
        .toList();

    setState(() {
      _messages = messages;
    });
  }

  void onMessageReceived(types.Message message) {
    if (_messages.every((element) => element.id != message.id)) {
      _addMessage(message);
    }
  }

  @override
  void dispose() {
    widget.webSocketChannel.sink.close(status.goingAway);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Scaffold(
        body: Chat(
          messages: _messages,
          onAttachmentPressed: _handleAttachmentPressed,
          onMessageTap: _handleMessageTap,
          onPreviewDataFetched: _handlePreviewDataFetched,
          onSendPressed: _handleSendPressed,
          showUserAvatars: true,
          showUserNames: true,
          user: widget.me,
          isLeftStatus: true,
          theme: const DefaultChatTheme(
            seenIcon: Text(
              'read',
              style: TextStyle(
                fontSize: 10.0,
              ),
            ),
          ),
        ),
      );
}

Dependencies below:

name: example
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 1.0.0+1

environment:
  sdk: '>=2.19.0 <4.0.0'

dependencies:
  cupertino_icons: ^1.0.5
  file_picker: ^5.3.2
  flutter:
    sdk: flutter
  flutter_chat_types: ^3.6.2
  flutter_chat_ui:
    path: ../
  http: '>=0.13.6 <2.0.0'
  image_picker: ^1.0.0
  intl: ^0.18.1
  mime: ^1.0.4
  open_filex: ^4.3.2
  path_provider: ^2.0.15
  uuid: ^3.0.7
  web_socket_channel: ^2.4.0

dev_dependencies:
  dart_code_metrics: ^5.7.5
  flutter_lints: ^2.0.2
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  assets:
    - assets/messages.json

env:

macOS: Sonoma 14.2.1

chrome 121.0.6167.160 arm64

ios emulator: iphone 15 pro max

I will be thankful for any tips!

Kirimatt commented 4 months ago

In addition, I've tried debug web mode and release. That also didn't have any effect.

Kirimatt commented 4 months ago

Flutter doctor:

[!] Flutter (Channel stable, 3.16.9, on macOS 14.2.1 23C71 darwin-arm64, locale ru-RU)
    • Flutter version 3.16.9 on channel stable at /Users/a.segizbaev/Downloads/flutter
    ! Warning: `dart` on your path resolves to /opt/homebrew/Cellar/dart/3.2.6/libexec/bin/dart, which is not inside your current Flutter SDK checkout at /Users/a.segizbaev/Downloads/flutter. Consider adding /Users/a.segizbaev/Downloads/flutter/bin to the front of your path.
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 41456452f2 (4 weeks ago), 2024-01-25 10:06:23 -0800
    • Engine revision f40e976bed
    • Dart version 3.2.6
    • DevTools version 2.28.5
    • If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks and upgrades.

[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /Users/a.segizbaev/Library/Android/sdk
    ✗ cmdline-tools component is missing
      Run `path/to/sdkmanager --install "cmdline-tools;latest"`
      See https://developer.android.com/studio/command-line for more details.
    ✗ Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
      See https://flutter.dev/docs/get-started/install/macos#android-setup for more details.

[✓] Xcode - develop for iOS and macOS (Xcode 15.2)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15C500b
    • CocoaPods version 1.15.0

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2023.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314)

[✓] IntelliJ IDEA Ultimate Edition (version 2022.2)
    • IntelliJ at /Applications/IntelliJ IDEA.app
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart

[✓] IntelliJ IDEA Community Edition (version 2023.1.3)
    • IntelliJ at /Users/a.segizbaev/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/231.9161.38/IntelliJ IDEA CE.app
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart

[✓] IntelliJ IDEA Ultimate Edition (version 2023.1.3)
    • IntelliJ at /Users/a.segizbaev/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/231.9161.38/IntelliJ IDEA.app
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart

[✓] VS Code (version 1.85.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension can be installed from:
      🔨 https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter

[✓] Connected device (3 available)
    • iPhone 15 Pro Max (mobile) • 930AA252-4AA1-4C18-A283-AD221E5B5DC6 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-17-2 (simulator)
    • macOS (desktop)            • macos                                • darwin-arm64   • macOS 14.2.1 23C71 darwin-arm64
    • Chrome (web)               • chrome                               • web-javascript • Google Chrome 122.0.6261.69

[✓] Network resources
    • All expected network resources are available.

! Doctor found issues in 2 categories.
Process finished with exit code 0
Kirimatt commented 4 months ago

updated dependencies, but it wasn't affected at all...

environment:
  sdk: '>=3.3.0 <4.0.0' 

web_socket_channel: ^2.4.4
Kirimatt commented 4 months ago

Yeah, it's mine misleading.

After changing security protocols for WebSocketChannel, as I inspected, browser didn't receive answer for changed protocol and, eventually, close the connection.

Removing sec protocol helps me get proper connection.

Sorry for spamming.