Maintain basic messaging websocket

This commit is contained in:
2024-04-18 22:33:55 +08:00
parent 8943f089f2
commit 94f9076abb
52 changed files with 288 additions and 101 deletions

View File

@@ -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
View 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
View 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;
}
}

View File

@@ -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,35 +102,43 @@ class _ChatScreenState extends State<ChatScreen> {
return IndentWrapper(
hideDrawer: true,
title: _channelMeta?.name ?? "Loading...",
child: Column(
children: [
Expanded(
child: PagedListView<int, Message>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Message>(
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<int, Message>(
reverse: true,
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Message>(
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),
),
);
}

View 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;
}
}

View File

@@ -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 {