✨ Chat messaging
This commit is contained in:
parent
5b45718ebd
commit
9cb2b9122e
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/providers/account.dart';
|
import 'package:solian/providers/account.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/providers/chat.dart';
|
||||||
import 'package:solian/providers/content/attachment.dart';
|
import 'package:solian/providers/content/attachment.dart';
|
||||||
import 'package:solian/providers/content/channel.dart';
|
import 'package:solian/providers/content/channel.dart';
|
||||||
import 'package:solian/providers/content/post.dart';
|
import 'package:solian/providers/content/post.dart';
|
||||||
@ -36,6 +37,7 @@ class SolianApp extends StatelessWidget {
|
|||||||
Get.lazyPut(() => FriendProvider());
|
Get.lazyPut(() => FriendProvider());
|
||||||
Get.lazyPut(() => PostProvider());
|
Get.lazyPut(() => PostProvider());
|
||||||
Get.lazyPut(() => AttachmentProvider());
|
Get.lazyPut(() => AttachmentProvider());
|
||||||
|
Get.lazyPut(() => ChatProvider());
|
||||||
Get.lazyPut(() => AccountProvider());
|
Get.lazyPut(() => AccountProvider());
|
||||||
Get.lazyPut(() => ChannelProvider());
|
Get.lazyPut(() => ChannelProvider());
|
||||||
Get.lazyPut(() => RealmProvider());
|
Get.lazyPut(() => RealmProvider());
|
||||||
@ -44,6 +46,7 @@ class SolianApp extends StatelessWidget {
|
|||||||
auth.isAuthorized.then((value) async {
|
auth.isAuthorized.then((value) async {
|
||||||
if (value) {
|
if (value) {
|
||||||
Get.find<AccountProvider>().connect();
|
Get.find<AccountProvider>().connect();
|
||||||
|
Get.find<ChatProvider>().connect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:solian/models/account.dart';
|
import 'package:solian/models/account.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
|
|
||||||
class Message {
|
class Message {
|
||||||
int id;
|
int id;
|
||||||
|
String uuid;
|
||||||
DateTime createdAt;
|
DateTime createdAt;
|
||||||
DateTime updatedAt;
|
DateTime updatedAt;
|
||||||
DateTime? deletedAt;
|
DateTime? deletedAt;
|
||||||
Map<String, dynamic> content;
|
Map<String, dynamic> content;
|
||||||
Map<String, dynamic>? metadata;
|
|
||||||
String type;
|
String type;
|
||||||
List<String>? attachments;
|
List<int>? attachments;
|
||||||
Channel? channel;
|
Channel? channel;
|
||||||
Sender sender;
|
Sender sender;
|
||||||
int? replyId;
|
int? replyId;
|
||||||
@ -23,11 +21,11 @@ class Message {
|
|||||||
|
|
||||||
Message({
|
Message({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.uuid,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
this.deletedAt,
|
this.deletedAt,
|
||||||
required this.content,
|
required this.content,
|
||||||
required this.metadata,
|
|
||||||
required this.type,
|
required this.type,
|
||||||
this.attachments,
|
this.attachments,
|
||||||
this.channel,
|
this.channel,
|
||||||
@ -40,13 +38,15 @@ class Message {
|
|||||||
|
|
||||||
factory Message.fromJson(Map<String, dynamic> json) => Message(
|
factory Message.fromJson(Map<String, dynamic> json) => Message(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
|
uuid: json['uuid'],
|
||||||
createdAt: DateTime.parse(json['created_at']),
|
createdAt: DateTime.parse(json['created_at']),
|
||||||
updatedAt: DateTime.parse(json['updated_at']),
|
updatedAt: DateTime.parse(json['updated_at']),
|
||||||
deletedAt: json['deleted_at'],
|
deletedAt: json['deleted_at'],
|
||||||
content: json['content'],
|
content: json['content'],
|
||||||
metadata: json['metadata'],
|
|
||||||
type: json['type'],
|
type: json['type'],
|
||||||
attachments: json['attachments'],
|
attachments: json["attachments"] != null
|
||||||
|
? List<int>.from(json["attachments"])
|
||||||
|
: null,
|
||||||
channel: Channel.fromJson(json['channel']),
|
channel: Channel.fromJson(json['channel']),
|
||||||
sender: Sender.fromJson(json['sender']),
|
sender: Sender.fromJson(json['sender']),
|
||||||
replyId: json['reply_id'],
|
replyId: json['reply_id'],
|
||||||
@ -59,11 +59,11 @@ class Message {
|
|||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'id': id,
|
'id': id,
|
||||||
|
'uuid': uuid,
|
||||||
'created_at': createdAt.toIso8601String(),
|
'created_at': createdAt.toIso8601String(),
|
||||||
'updated_at': updatedAt.toIso8601String(),
|
'updated_at': updatedAt.toIso8601String(),
|
||||||
'deleted_at': deletedAt,
|
'deleted_at': deletedAt,
|
||||||
'content': content,
|
'content': content,
|
||||||
'metadata': metadata,
|
|
||||||
'type': type,
|
'type': type,
|
||||||
'attachments': attachments,
|
'attachments': attachments,
|
||||||
'channel': channel?.toJson(),
|
'channel': channel?.toJson(),
|
||||||
|
@ -144,7 +144,7 @@ class AccountProvider extends GetxController {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (!await auth.isAuthorized) return;
|
if (!await auth.isAuthorized) return;
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get/get_connect/http/src/request/request.dart';
|
import 'package:get/get_connect/http/src/request/request.dart';
|
||||||
import 'package:solian/providers/account.dart';
|
import 'package:solian/providers/account.dart';
|
||||||
|
import 'package:solian/providers/chat.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
import 'package:oauth2/oauth2.dart' as oauth2;
|
import 'package:oauth2/oauth2.dart' as oauth2;
|
||||||
|
|
||||||
@ -98,6 +99,7 @@ class AuthProvider extends GetConnect {
|
|||||||
|
|
||||||
Get.find<AccountProvider>().connect();
|
Get.find<AccountProvider>().connect();
|
||||||
Get.find<AccountProvider>().notifyPrefetch();
|
Get.find<AccountProvider>().notifyPrefetch();
|
||||||
|
Get.find<ChatProvider>().connect();
|
||||||
|
|
||||||
return credentials!;
|
return credentials!;
|
||||||
}
|
}
|
||||||
@ -105,6 +107,7 @@ class AuthProvider extends GetConnect {
|
|||||||
void signout() {
|
void signout() {
|
||||||
_cacheUserProfileResponse = null;
|
_cacheUserProfileResponse = null;
|
||||||
|
|
||||||
|
Get.find<ChatProvider>().disconnect();
|
||||||
Get.find<AccountProvider>().disconnect();
|
Get.find<AccountProvider>().disconnect();
|
||||||
Get.find<AccountProvider>().notifications.clear();
|
Get.find<AccountProvider>().notifications.clear();
|
||||||
Get.find<AccountProvider>().notificationUnread.value = 0;
|
Get.find<AccountProvider>().notificationUnread.value = 0;
|
||||||
@ -121,7 +124,7 @@ class AuthProvider extends GetConnect {
|
|||||||
return _cacheUserProfileResponse!;
|
return _cacheUserProfileResponse!;
|
||||||
}
|
}
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
||||||
client.httpClient.addAuthenticator(requestAuthenticator);
|
client.httpClient.addAuthenticator(requestAuthenticator);
|
||||||
|
|
||||||
|
67
lib/providers/chat.dart
Normal file
67
lib/providers/chat.dart
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/packet.dart';
|
||||||
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/services.dart';
|
||||||
|
import 'package:web_socket_channel/io.dart';
|
||||||
|
|
||||||
|
class ChatProvider extends GetxController {
|
||||||
|
RxBool isConnected = false.obs;
|
||||||
|
RxBool isConnecting = false.obs;
|
||||||
|
|
||||||
|
IOWebSocketChannel? websocket;
|
||||||
|
|
||||||
|
StreamController<NetworkPackage> stream = StreamController.broadcast();
|
||||||
|
|
||||||
|
void connect({noRetry = false}) async {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||||
|
|
||||||
|
if (auth.credentials == null) await auth.loadCredentials();
|
||||||
|
|
||||||
|
final uri = Uri.parse(
|
||||||
|
'${ServiceFinder.services['messaging']}/api/ws?tk=${auth.credentials!.accessToken}'
|
||||||
|
.replaceFirst('http', 'ws'),
|
||||||
|
);
|
||||||
|
|
||||||
|
isConnecting.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
websocket = IOWebSocketChannel.connect(uri);
|
||||||
|
await websocket?.ready;
|
||||||
|
} catch (e) {
|
||||||
|
if (!noRetry) {
|
||||||
|
await auth.refreshCredentials();
|
||||||
|
return connect(noRetry: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listen();
|
||||||
|
|
||||||
|
isConnected.value = true;
|
||||||
|
isConnecting.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void disconnect() {
|
||||||
|
websocket?.sink.close(WebSocketStatus.normalClosure);
|
||||||
|
isConnected.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void listen() {
|
||||||
|
websocket?.stream.listen(
|
||||||
|
(event) {
|
||||||
|
final packet = NetworkPackage.fromJson(jsonDecode(event));
|
||||||
|
stream.sink.add(packet);
|
||||||
|
},
|
||||||
|
onDone: () {
|
||||||
|
isConnected.value = false;
|
||||||
|
},
|
||||||
|
onError: (err) {
|
||||||
|
isConnected.value = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -37,7 +37,7 @@ class AttachmentProvider extends GetConnect {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ class AttachmentProvider extends GetConnect {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ class AttachmentProvider extends GetConnect {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ class ChannelProvider extends GetxController {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
|
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
|
||||||
|
|
||||||
final resp = await client.get('/api/channels/$realm/$alias');
|
final resp = await client.get('/api/channels/$realm/$alias');
|
||||||
@ -22,7 +22,7 @@ class ChannelProvider extends GetxController {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
|
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ class RealmProvider extends GetxController {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (markList.isNotEmpty) {
|
if (markList.isNotEmpty) {
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
context.showErrorDialog(e);
|
context.showErrorDialog(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class _SignUpPopupState extends State<SignUpPopup> {
|
|||||||
nickname.isEmpty ||
|
nickname.isEmpty ||
|
||||||
password.isEmpty) return;
|
password.isEmpty) return;
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
||||||
final resp = await client.post('/api/users', {
|
final resp = await client.post('/api/users', {
|
||||||
'name': username,
|
'name': username,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -5,12 +7,15 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
import 'package:solian/models/message.dart';
|
import 'package:solian/models/message.dart';
|
||||||
|
import 'package:solian/models/packet.dart';
|
||||||
import 'package:solian/models/pagination.dart';
|
import 'package:solian/models/pagination.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/providers/chat.dart';
|
||||||
import 'package:solian/providers/content/channel.dart';
|
import 'package:solian/providers/content/channel.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/chat/chat_message.dart';
|
import 'package:solian/widgets/chat/chat_message.dart';
|
||||||
|
import 'package:solian/widgets/chat/chat_message_input.dart';
|
||||||
|
|
||||||
class ChannelChatScreen extends StatefulWidget {
|
class ChannelChatScreen extends StatefulWidget {
|
||||||
final String alias;
|
final String alias;
|
||||||
@ -30,6 +35,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
|
|
||||||
Channel? _channel;
|
Channel? _channel;
|
||||||
|
StreamSubscription<NetworkPackage>? _subscription;
|
||||||
|
|
||||||
final PagingController<int, Message> _pagingController =
|
final PagingController<int, Message> _pagingController =
|
||||||
PagingController(firstPageKey: 0);
|
PagingController(firstPageKey: 0);
|
||||||
@ -53,7 +59,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (!await auth.isAuthorized) return;
|
if (!await auth.isAuthorized) return;
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
|
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
@ -76,10 +82,47 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void listenMessages() {
|
||||||
|
final ChatProvider provider = Get.find();
|
||||||
|
_subscription = provider.stream.stream.listen((event) {
|
||||||
|
switch (event.method) {
|
||||||
|
case 'messages.new':
|
||||||
|
final payload = Message.fromJson(event.payload!);
|
||||||
|
if (payload.channelId == _channel?.id) {
|
||||||
|
final idx = _pagingController.itemList
|
||||||
|
?.indexWhere((e) => e.uuid == payload.uuid);
|
||||||
|
if ((idx ?? -1) >= 0) {
|
||||||
|
_pagingController.itemList?[idx!] = payload;
|
||||||
|
} else {
|
||||||
|
_pagingController.itemList?.insert(0, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'messages.update':
|
||||||
|
final payload = Message.fromJson(event.payload!);
|
||||||
|
if (payload.channelId == _channel?.id) {
|
||||||
|
_pagingController.itemList
|
||||||
|
?.map((x) => x.id == payload.id ? payload : x)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'messages.burnt':
|
||||||
|
final payload = Message.fromJson(event.payload!);
|
||||||
|
if (payload.channelId == _channel?.id) {
|
||||||
|
_pagingController.itemList = _pagingController.itemList
|
||||||
|
?.where((x) => x.id != payload.id)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
bool checkMessageMergeable(Message? a, Message? b) {
|
bool checkMessageMergeable(Message? a, Message? b) {
|
||||||
if (a?.replyTo != null) return false;
|
if (a?.replyTo != null) return false;
|
||||||
if (a == null || b == null) return false;
|
if (a == null || b == null) return false;
|
||||||
if (a.senderId != b.senderId) return false;
|
if (a.sender.account.id != b.sender.account.id) return false;
|
||||||
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +150,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
),
|
),
|
||||||
child: ChatMessage(
|
child: ChatMessage(
|
||||||
item: item,
|
item: item,
|
||||||
isCompact: isMerged,
|
isMerged: isMerged,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onLongPress: () {},
|
onLongPress: () {},
|
||||||
@ -124,6 +167,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
getChannel().then((_) {
|
getChannel().then((_) {
|
||||||
|
listenMessages();
|
||||||
_pagingController.addPageRequestListener(getMessages);
|
_pagingController.addPageRequestListener(getMessages);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -150,22 +194,47 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Column(
|
||||||
child: PagedListView<int, Message>(
|
children: [
|
||||||
reverse: true,
|
Expanded(
|
||||||
pagingController: _pagingController,
|
child: PagedListView<int, Message>(
|
||||||
builderDelegate: PagedChildBuilderDelegate<Message>(
|
reverse: true,
|
||||||
animateTransitions: true,
|
clipBehavior: Clip.none,
|
||||||
transitionDuration: 350.ms,
|
pagingController: _pagingController,
|
||||||
itemBuilder: chatHistoryBuilder,
|
builderDelegate: PagedChildBuilderDelegate<Message>(
|
||||||
noItemsFoundIndicatorBuilder: (_) => Container(),
|
animateTransitions: true,
|
||||||
|
transitionDuration: 350.ms,
|
||||||
|
itemBuilder: chatHistoryBuilder,
|
||||||
|
noItemsFoundIndicatorBuilder: (_) => Container(),
|
||||||
|
),
|
||||||
|
).paddingOnly(bottom: 64),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
child: ChatMessageInput(
|
||||||
|
realm: widget.realm,
|
||||||
|
channel: _channel!,
|
||||||
|
onSent: (Message item) {
|
||||||
|
setState(() {
|
||||||
|
_pagingController.itemList?.insert(0, item);
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
|||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
|
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
|
|||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['interactive'];
|
client.httpClient.baseUrl = ServiceFinder.services['interactive'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
|
@ -103,6 +103,7 @@ class SolianMessages extends Translations {
|
|||||||
'channelTypeDirect': 'DM',
|
'channelTypeDirect': 'DM',
|
||||||
'messageDecoding': 'Decoding...',
|
'messageDecoding': 'Decoding...',
|
||||||
'messageDecodeFailed': 'Unable to decode: @message',
|
'messageDecodeFailed': 'Unable to decode: @message',
|
||||||
|
'messageInputPlaceholder': 'Message @channel...',
|
||||||
},
|
},
|
||||||
'zh_CN': {
|
'zh_CN': {
|
||||||
'hide': '隐藏',
|
'hide': '隐藏',
|
||||||
@ -197,6 +198,7 @@ class SolianMessages extends Translations {
|
|||||||
'channelTypeDirect': '私信聊天',
|
'channelTypeDirect': '私信聊天',
|
||||||
'messageDecoding': '解码信息中…',
|
'messageDecoding': '解码信息中…',
|
||||||
'messageDecodeFailed': '解码信息失败:@message',
|
'messageDecodeFailed': '解码信息失败:@message',
|
||||||
|
'messageInputPlaceholder': '在 @channel 发信息…',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,19 @@ import 'package:get/get.dart';
|
|||||||
import 'package:solian/models/message.dart';
|
import 'package:solian/models/message.dart';
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
import 'package:timeago/timeago.dart' show format;
|
import 'package:timeago/timeago.dart' show format;
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class ChatMessage extends StatelessWidget {
|
class ChatMessage extends StatelessWidget {
|
||||||
final Message item;
|
final Message item;
|
||||||
final bool isCompact;
|
final bool isCompact;
|
||||||
|
final bool isMerged;
|
||||||
|
|
||||||
const ChatMessage({super.key, required this.item, required this.isCompact});
|
const ChatMessage({
|
||||||
|
super.key,
|
||||||
|
required this.item,
|
||||||
|
this.isMerged = false,
|
||||||
|
this.isCompact = false,
|
||||||
|
});
|
||||||
|
|
||||||
Future<String?> decodeContent(Map<String, dynamic> content) async {
|
Future<String?> decodeContent(Map<String, dynamic> content) async {
|
||||||
String? text;
|
String? text;
|
||||||
@ -27,78 +34,117 @@ class ChatMessage extends StatelessWidget {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Widget buildContent() {
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final hasAttachment = item.attachments?.isNotEmpty ?? false;
|
final hasAttachment = item.attachments?.isNotEmpty ?? false;
|
||||||
|
|
||||||
return Column(
|
return FutureBuilder(
|
||||||
children: [
|
future: decodeContent(item.content),
|
||||||
Row(
|
builder: (context, snapshot) {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
if (!snapshot.hasData) {
|
||||||
children: [
|
return Opacity(
|
||||||
AccountAvatar(content: item.sender.account.avatar),
|
opacity: 0.8,
|
||||||
Expanded(
|
child: Row(
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
const Icon(Icons.more_horiz),
|
||||||
children: [
|
const SizedBox(width: 4),
|
||||||
Row(
|
Text('messageDecoding'.tr)
|
||||||
children: [
|
],
|
||||||
Text(
|
|
||||||
item.sender.account.nick,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(format(item.createdAt, locale: 'en_short'))
|
|
||||||
],
|
|
||||||
).paddingSymmetric(horizontal: 12),
|
|
||||||
FutureBuilder(
|
|
||||||
future: decodeContent(item.content),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return Opacity(
|
|
||||||
opacity: 0.8,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.more_horiz),
|
|
||||||
Text('messageDecoding'.tr)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.animate(onPlay: (c) => c.repeat())
|
|
||||||
.fade(begin: 0, end: 1);
|
|
||||||
} else if (snapshot.hasError) {
|
|
||||||
return Opacity(
|
|
||||||
opacity: 0.9,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.close),
|
|
||||||
Text(
|
|
||||||
'messageDecodeFailed'.trParams(
|
|
||||||
{'message': snapshot.error.toString()}),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Markdown(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
data: snapshot.data ?? '',
|
|
||||||
padding: const EdgeInsets.all(0),
|
|
||||||
).paddingOnly(
|
|
||||||
left: 12,
|
|
||||||
right: 12,
|
|
||||||
top: 2,
|
|
||||||
bottom: hasAttachment ? 4 : 0,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
).animate(onPlay: (c) => c.repeat()).fade(begin: 0, end: 1);
|
||||||
),
|
} else if (snapshot.hasError) {
|
||||||
],
|
return Opacity(
|
||||||
|
opacity: 0.9,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.close),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'messageDecodeFailed'
|
||||||
|
.trParams({'message': snapshot.error.toString()}),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Markdown(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
data: snapshot.data ?? '',
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
onTapLink: (text, href, title) async {
|
||||||
|
if (href == null) return;
|
||||||
|
await launchUrlString(
|
||||||
|
href,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).paddingOnly(
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
top: 2,
|
||||||
|
bottom: hasAttachment ? 4 : 0,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget widget;
|
||||||
|
if (isMerged) {
|
||||||
|
widget = buildContent().paddingOnly(left: 40);
|
||||||
|
} else if (isCompact) {
|
||||||
|
widget = Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AccountAvatar(content: item.sender.account.avatar, radius: 8),
|
||||||
|
Text(
|
||||||
|
item.sender.account.nick,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(format(item.createdAt, locale: 'en_short')),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
buildContent(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
widget = Column(
|
||||||
|
key: Key('m${item.uuid}'),
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
AccountAvatar(content: item.sender.account.avatar),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.sender.account.nick,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(format(item.createdAt, locale: 'en_short'))
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 12),
|
||||||
|
buildContent(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.isSending) {
|
||||||
|
return Opacity(opacity: 0.65, child: widget);
|
||||||
|
} else {
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
158
lib/widgets/chat/chat_message_input.dart
Normal file
158
lib/widgets/chat/chat_message_input.dart
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/exts.dart';
|
||||||
|
import 'package:solian/models/account.dart';
|
||||||
|
import 'package:solian/models/channel.dart';
|
||||||
|
import 'package:solian/models/message.dart';
|
||||||
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/services.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
class ChatMessageInput extends StatefulWidget {
|
||||||
|
final Message? edit;
|
||||||
|
final Message? reply;
|
||||||
|
final Channel channel;
|
||||||
|
final String realm;
|
||||||
|
final Function(Message) onSent;
|
||||||
|
|
||||||
|
const ChatMessageInput({
|
||||||
|
super.key,
|
||||||
|
this.edit,
|
||||||
|
this.reply,
|
||||||
|
required this.channel,
|
||||||
|
required this.realm,
|
||||||
|
required this.onSent,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChatMessageInput> createState() => _ChatMessageInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||||
|
final TextEditingController _textController = TextEditingController();
|
||||||
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
|
||||||
|
List<int> _attachments = List.empty(growable: true);
|
||||||
|
|
||||||
|
Map<String, dynamic> encodeMessage(String content) {
|
||||||
|
// TODO Impl E2EE
|
||||||
|
|
||||||
|
return {
|
||||||
|
'value': content,
|
||||||
|
'keypair_id': null,
|
||||||
|
'algorithm': 'plain',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sendMessage() async {
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
final prof = await auth.getProfile();
|
||||||
|
if (!await auth.isAuthorized) return;
|
||||||
|
|
||||||
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
|
client.httpClient.baseUrl = ServiceFinder.services['messaging'];
|
||||||
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
|
final payload = {
|
||||||
|
'uuid': const Uuid().v4(),
|
||||||
|
'type': 'm.text',
|
||||||
|
'content': encodeMessage(_textController.value.text),
|
||||||
|
'attachments': _attachments,
|
||||||
|
'reply_to': widget.reply?.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
// The mock data
|
||||||
|
final sender = Sender(
|
||||||
|
id: 0,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
account: Account.fromJson(prof.body),
|
||||||
|
channelId: widget.channel.id,
|
||||||
|
accountId: prof.body['id'],
|
||||||
|
notify: 0,
|
||||||
|
);
|
||||||
|
final message = Message(
|
||||||
|
id: 0,
|
||||||
|
uuid: payload['uuid'] as String,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
content: payload['content'] as Map<String, dynamic>,
|
||||||
|
type: payload['type'] as String,
|
||||||
|
sender: sender,
|
||||||
|
replyId: widget.reply?.id,
|
||||||
|
replyTo: widget.reply,
|
||||||
|
channelId: widget.channel.id,
|
||||||
|
senderId: sender.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
widget.onSent(message);
|
||||||
|
resetInput();
|
||||||
|
|
||||||
|
Response resp;
|
||||||
|
if (widget.edit != null) {
|
||||||
|
resp = await client.put(
|
||||||
|
'/api/channels/${widget.realm}/${widget.channel.alias}/messages/${widget.edit!.id}',
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resp = await client.post(
|
||||||
|
'/api/channels/${widget.realm}/${widget.channel.alias}/messages',
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
context.showErrorDialog(resp.bodyString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetInput() {
|
||||||
|
_textController.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const double height = 56;
|
||||||
|
const borderRadius = BorderRadius.all(Radius.circular(height / 2));
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
elevation: 2,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
child: SizedBox(
|
||||||
|
height: height,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _textController,
|
||||||
|
focusNode: _focusNode,
|
||||||
|
maxLines: null,
|
||||||
|
autocorrect: true,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
decoration: InputDecoration.collapsed(
|
||||||
|
hintText: 'messageInputPlaceholder'.trParams(
|
||||||
|
{'channel': '#${widget.channel.alias}'},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onSubmitted: (_) => sendMessage(),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.send),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
onPressed: () => sendMessage(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).paddingOnly(left: 16, right: 4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -154,7 +154,7 @@ class _PostDeletionDialogState extends State<PostDeletionDialog> {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (!await auth.isAuthorized) return;
|
if (!await auth.isAuthorized) return;
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['interactive'];
|
client.httpClient.baseUrl = ServiceFinder.services['interactive'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import 'package:solian/widgets/account/account_avatar.dart';
|
|||||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||||
import 'package:solian/widgets/posts/post_quick_action.dart';
|
import 'package:solian/widgets/posts/post_quick_action.dart';
|
||||||
import 'package:timeago/timeago.dart' show format;
|
import 'package:timeago/timeago.dart' show format;
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class PostItem extends StatefulWidget {
|
class PostItem extends StatefulWidget {
|
||||||
final Post item;
|
final Post item;
|
||||||
@ -132,6 +133,13 @@ class _PostItemState extends State<PostItem> {
|
|||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
data: item.content,
|
data: item.content,
|
||||||
padding: const EdgeInsets.all(0),
|
padding: const EdgeInsets.all(0),
|
||||||
|
onTapLink: (text, href, title) async {
|
||||||
|
if (href == null) return;
|
||||||
|
await launchUrlString(
|
||||||
|
href,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
},
|
||||||
).paddingOnly(
|
).paddingOnly(
|
||||||
left: 16,
|
left: 16,
|
||||||
right: 12,
|
right: 12,
|
||||||
|
@ -51,7 +51,7 @@ class _PostQuickActionState extends State<PostQuickAction> {
|
|||||||
if (_isSubmitting) return;
|
if (_isSubmitting) return;
|
||||||
if (!await auth.isAuthorized) return;
|
if (!await auth.isAuthorized) return;
|
||||||
|
|
||||||
final client = GetConnect();
|
final client = GetConnect(maxAuthRetries: 3);
|
||||||
client.httpClient.baseUrl = ServiceFinder.services['interactive'];
|
client.httpClient.baseUrl = ServiceFinder.services['interactive'];
|
||||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user