diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f44eb24..81dc265 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ + android:icon="@mipmap/launcher_icon"> NavigationProvider()), Provider(create: (_) => AuthProvider()), + Provider(create: (_) => ChatProvider()), ], child: child, ); diff --git a/lib/models/packet.dart b/lib/models/packet.dart new file mode 100644 index 0000000..d0511cb --- /dev/null +++ b/lib/models/packet.dart @@ -0,0 +1,23 @@ +class NetworkPackage { + String method; + String? message; + Map? payload; + + NetworkPackage({ + required this.method, + this.message, + this.payload, + }); + + factory NetworkPackage.fromJson(Map json) => NetworkPackage( + method: json["w"], + message: json["m"], + payload: json["p"], + ); + + Map toJson() => { + "w": method, + "m": message, + "p": payload, + }; +} \ No newline at end of file diff --git a/lib/providers/chat.dart b/lib/providers/chat.dart new file mode 100644 index 0000000..1b69ef9 --- /dev/null +++ b/lib/providers/chat.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +import 'package:solian/providers/auth.dart'; +import 'package:solian/utils/service_url.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +class ChatProvider { + bool isOpened = false; + + Future connect(AuthProvider auth) async { + if (auth.client == null) await auth.pickClient(); + if (!await auth.isAuthorized()) return null; + + await auth.refreshToken(); + + var ori = getRequestUri('messaging', '/api/unified'); + var uri = Uri( + scheme: ori.scheme.replaceFirst('http', 'ws'), + host: ori.host, + path: ori.path, + queryParameters: {'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)}, + ); + + final channel = WebSocketChannel.connect(uri); + await channel.ready; + + return channel; + } +} diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index c3989b8..5d0712b 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:provider/provider.dart'; @@ -9,6 +8,7 @@ import 'package:solian/models/message.dart'; import 'package:solian/models/pagination.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/utils/service_url.dart'; +import 'package:solian/widgets/chat/maintainer.dart'; import 'package:solian/widgets/chat/message.dart'; import 'package:solian/widgets/chat/message_editor.dart'; import 'package:solian/widgets/indent_wrapper.dart'; @@ -78,6 +78,14 @@ class _ChatScreenState extends State { return a.createdAt.difference(b.createdAt).inMinutes <= 5; } + void addMessage(Message item) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _pagingController.itemList?.insert(0, item); + }); + }); + } + @override void initState() { Future.delayed(Duration.zero, () { @@ -94,35 +102,43 @@ class _ChatScreenState extends State { return IndentWrapper( hideDrawer: true, title: _channelMeta?.name ?? "Loading...", - child: Column( - children: [ - Expanded( - child: PagedListView( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) { - bool isMerged = false, hasMerged = false; - if (index > 0) { - isMerged = getMessageMergeable(_pagingController.itemList?[index - 1], item); - } - if (index + 1 < (_pagingController.itemList?.length ?? 0)) { - hasMerged = getMessageMergeable(item, _pagingController.itemList?[index + 1]); - } - return Container( - padding: EdgeInsets.only( - top: !isMerged ? 8 : 0, - bottom: !hasMerged ? 8 : 0, - left: 12, - right: 12, - ), - child: ChatMessage(item: item, underMerged: isMerged), - ); - }, + child: ChatMaintainer( + child: Column( + children: [ + Expanded( + child: PagedListView( + reverse: true, + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + bool isMerged = false, hasMerged = false; + if (index > 0) { + hasMerged = getMessageMergeable(_pagingController.itemList?[index - 1], item); + } + if (index + 1 < (_pagingController.itemList?.length ?? 0)) { + isMerged = getMessageMergeable(item, _pagingController.itemList?[index + 1]); + } + return Container( + padding: EdgeInsets.only( + top: !isMerged ? 8 : 0, + bottom: !hasMerged ? 8 : 0, + left: 12, + right: 12, + ), + child: ChatMessage( + key: Key('m${item.id}'), + item: item, + underMerged: isMerged, + ), + ); + }, + ), ), ), - ), - ChatMessageEditor(channel: widget.alias), - ], + ChatMessageEditor(channel: widget.alias), + ], + ), + onNewMessage: (message) => addMessage(message), ), ); } diff --git a/lib/widgets/chat/maintainer.dart b/lib/widgets/chat/maintainer.dart new file mode 100644 index 0000000..db8403e --- /dev/null +++ b/lib/widgets/chat/maintainer.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:solian/models/message.dart'; +import 'package:solian/models/packet.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/providers/chat.dart'; + +class ChatMaintainer extends StatefulWidget { + final Widget child; + final Function(Message val) onNewMessage; + + const ChatMaintainer({super.key, required this.child, required this.onNewMessage}); + + @override + State createState() => _ChatMaintainerState(); +} + +class _ChatMaintainerState extends State { + @override + void initState() { + Future.delayed(Duration.zero, () { + final auth = context.read(); + final chat = context.read(); + + chat.connect(auth).then((snapshot) { + snapshot!.stream.listen((event) { + final result = NetworkPackage.fromJson(jsonDecode(event)); + switch (result.method) { + case 'messages.new': + widget.onNewMessage(Message.fromJson(result.payload!)); + } + }); + }); + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/widgets/chat/message.dart b/lib/widgets/chat/message.dart index 491fcf8..6e03d9e 100644 --- a/lib/widgets/chat/message.dart +++ b/lib/widgets/chat/message.dart @@ -46,7 +46,11 @@ class ChatMessage extends StatelessWidget { return Row( children: [ const SizedBox(width: 40), - Expanded(child: contentPart), + Expanded( + child: Column( + children: [contentPart, renderAttachment()], + ), + ), ], ); } else { diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f..96d3fee 100644 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" + "info": { + "version": 1, + "author": "xcode" }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] +} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 82b6f9d..235214a 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 13b35eb..ea10a88 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 0a3f5fa..170be8c 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bdb5722..abec606 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index f083318..39fee4e 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index 326c0e7..bc754fe 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 2f1632c..c018756 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/pubspec.lock b/pubspec.lock index dd57944..bd915bf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" + source: hosted + version: "0.4.1" clock: dependency: transitive description: @@ -166,6 +182,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + flutter_launcher_icons: + dependency: "direct main" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" flutter_lints: dependency: "direct dev" description: @@ -397,6 +421,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" leak_tracker: dependency: transitive description: @@ -962,6 +994,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + web_socket_channel: + dependency: "direct main" + description: + name: web_socket_channel + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + url: "https://pub.dev" + source: hosted + version: "2.4.5" webview_flutter: dependency: "direct main" description: @@ -1018,6 +1058,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" sdks: dart: ">=3.3.3 <4.0.0" flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index 037eade..25cdbd0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,8 @@ dependencies: media_kit: ^1.1.10+1 media_kit_libs_video: ^1.0.4 hive_flutter: ^1.1.0 + flutter_launcher_icons: ^0.13.1 + web_socket_channel: ^2.4.5 dev_dependencies: flutter_test: @@ -111,3 +113,21 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages + +flutter_launcher_icons: + android: "launcher_icon" + ios: true + image_path: "assets/icon.png" + min_sdk_android: 21 + web: + generate: true + image_path: "assets/icon.png" + background_color: "#ffffff" + theme_color: "#4b5094" + windows: + generate: true + image_path: "assets/icon.png" + icon_size: 256 + macos: + generate: true + image_path: "assets/icon.png" \ No newline at end of file diff --git a/web/favicon.png b/web/favicon.png index 8aaa46a..170be8c 100644 Binary files a/web/favicon.png and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png index b749bfe..f44e587 100644 Binary files a/web/icons/Icon-192.png and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png index 88cfd48..bc754fe 100644 Binary files a/web/icons/Icon-512.png and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png index eb9b4d7..f44e587 100644 Binary files a/web/icons/Icon-maskable-192.png and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png index d69c566..bc754fe 100644 Binary files a/web/icons/Icon-maskable-512.png and b/web/icons/Icon-maskable-512.png differ diff --git a/web/manifest.json b/web/manifest.json index c506c4d..2e16699 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -3,8 +3,8 @@ "short_name": "solian", "start_url": ".", "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", + "background_color": "#ffffff", + "theme_color": "#4b5094", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, @@ -32,4 +32,4 @@ "purpose": "maskable" } ] -} +} \ No newline at end of file diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index c04e20c..abd09d9 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ