✨ Maintain basic messaging websocket
@ -2,7 +2,7 @@
|
||||
<application
|
||||
android:label="solian"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
BIN
android/app/src/main/res/mipmap-hdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
assets/icon.png
Executable file
After Width: | Height: | Size: 69 KiB |
@ -541,7 +541,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@ -598,7 +598,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 625 B |
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 958 B |
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 8.0 KiB |
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/chat.dart';
|
||||
import 'package:solian/providers/navigation.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/utils/timeago.dart';
|
||||
@ -37,6 +38,7 @@ class SolianApp extends StatelessWidget {
|
||||
providers: [
|
||||
Provider(create: (_) => NavigationProvider()),
|
||||
Provider(create: (_) => AuthProvider()),
|
||||
Provider(create: (_) => ChatProvider()),
|
||||
],
|
||||
child: child,
|
||||
);
|
||||
|
23
lib/models/packet.dart
Normal file
@ -0,0 +1,23 @@
|
||||
class NetworkPackage {
|
||||
String method;
|
||||
String? message;
|
||||
Map<String, dynamic>? payload;
|
||||
|
||||
NetworkPackage({
|
||||
required this.method,
|
||||
this.message,
|
||||
this.payload,
|
||||
});
|
||||
|
||||
factory NetworkPackage.fromJson(Map<String, dynamic> json) => NetworkPackage(
|
||||
method: json["w"],
|
||||
message: json["m"],
|
||||
payload: json["p"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"w": method,
|
||||
"m": message,
|
||||
"p": payload,
|
||||
};
|
||||
}
|
29
lib/providers/chat.dart
Normal file
@ -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<WebSocketChannel?> 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;
|
||||
}
|
||||
}
|
@ -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<ChatScreen> {
|
||||
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,19 +102,21 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
return IndentWrapper(
|
||||
hideDrawer: true,
|
||||
title: _channelMeta?.name ?? "Loading...",
|
||||
child: ChatMaintainer(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PagedListView<int, Message>(
|
||||
reverse: true,
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<Message>(
|
||||
itemBuilder: (context, item, index) {
|
||||
bool isMerged = false, hasMerged = false;
|
||||
if (index > 0) {
|
||||
isMerged = getMessageMergeable(_pagingController.itemList?[index - 1], item);
|
||||
hasMerged = getMessageMergeable(_pagingController.itemList?[index - 1], item);
|
||||
}
|
||||
if (index + 1 < (_pagingController.itemList?.length ?? 0)) {
|
||||
hasMerged = getMessageMergeable(item, _pagingController.itemList?[index + 1]);
|
||||
isMerged = getMessageMergeable(item, _pagingController.itemList?[index + 1]);
|
||||
}
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
@ -115,7 +125,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
left: 12,
|
||||
right: 12,
|
||||
),
|
||||
child: ChatMessage(item: item, underMerged: isMerged),
|
||||
child: ChatMessage(
|
||||
key: Key('m${item.id}'),
|
||||
item: item,
|
||||
underMerged: isMerged,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -124,6 +138,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
ChatMessageEditor(channel: widget.alias),
|
||||
],
|
||||
),
|
||||
onNewMessage: (message) => addMessage(message),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
45
lib/widgets/chat/maintainer.dart
Normal file
@ -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<ChatMaintainer> createState() => _ChatMaintainerState();
|
||||
}
|
||||
|
||||
class _ChatMaintainerState extends State<ChatMaintainer> {
|
||||
@override
|
||||
void initState() {
|
||||
Future.delayed(Duration.zero, () {
|
||||
final auth = context.read<AuthProvider>();
|
||||
final chat = context.read<ChatProvider>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -1,68 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_16.png",
|
||||
"scale" : "1x"
|
||||
"size": "16x16",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_16.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_32.png",
|
||||
"scale" : "2x"
|
||||
"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_32.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_64.png",
|
||||
"scale" : "2x"
|
||||
"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_128.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_256.png",
|
||||
"scale" : "2x"
|
||||
"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_256.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_512.png",
|
||||
"scale" : "2x"
|
||||
"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_512.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_1024.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"size": "512x512",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_1024.png",
|
||||
"scale": "2x"
|
||||
}
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 480 B |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.5 KiB |
48
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"
|
||||
|
20
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"
|
BIN
web/favicon.png
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 480 B |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 29 KiB |
@ -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,
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 13 KiB |