♻️ Use controller instead of state to manage history
This commit is contained in:
parent
2038d33a31
commit
aa8eec1a5a
48
lib/controllers/chat_history_controller.dart
Normal file
48
lib/controllers/chat_history_controller.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/message.dart';
|
||||
import 'package:solian/providers/message/helper.dart';
|
||||
import 'package:solian/providers/message/history.dart';
|
||||
|
||||
class ChatHistoryController {
|
||||
late final MessageHistoryDb database;
|
||||
|
||||
final RxList<LocalMessage> currentHistory = RxList.empty(growable: true);
|
||||
final RxInt totalHistoryCount = 0.obs;
|
||||
|
||||
initialize() async {
|
||||
database = await createHistoryDb();
|
||||
currentHistory.clear();
|
||||
}
|
||||
|
||||
Future<void> getMessages(Channel channel, String scope) async {
|
||||
totalHistoryCount.value = await database.syncMessages(channel, scope: scope);
|
||||
await syncHistory(channel);
|
||||
}
|
||||
|
||||
Future<void> syncHistory(Channel channel) async {
|
||||
currentHistory.replaceRange(0, currentHistory.length,
|
||||
await database.localMessages.findAllByChannel(channel.id));
|
||||
}
|
||||
|
||||
receiveMessage(Message remote) async {
|
||||
final entry = await database.receiveMessage(remote);
|
||||
totalHistoryCount.value++;
|
||||
currentHistory.add(entry);
|
||||
}
|
||||
|
||||
void replaceMessage(Message remote) async {
|
||||
final entry = await database.replaceMessage(remote);
|
||||
currentHistory.replaceRange(
|
||||
0,
|
||||
currentHistory.length,
|
||||
currentHistory.map((x) => x.id == entry.id ? entry : x),
|
||||
);
|
||||
}
|
||||
|
||||
void burnMessage(int id) async {
|
||||
await database.burnMessage(id);
|
||||
totalHistoryCount.value--;
|
||||
currentHistory.removeWhere((x) => x.id == id);
|
||||
}
|
||||
}
|
@ -81,24 +81,24 @@ class AccountBadge {
|
||||
});
|
||||
|
||||
factory AccountBadge.fromJson(Map<String, dynamic> json) => AccountBadge(
|
||||
id: json["id"],
|
||||
accountId: json["account_id"],
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
deletedAt: json["deleted_at"] != null
|
||||
? DateTime.parse(json["deleted_at"])
|
||||
id: json['id'],
|
||||
accountId: json['account_id'],
|
||||
updatedAt: DateTime.parse(json['updated_at']),
|
||||
createdAt: DateTime.parse(json['created_at']),
|
||||
deletedAt: json['deleted_at'] != null
|
||||
? DateTime.parse(json['deleted_at'])
|
||||
: null,
|
||||
metadata: json["metadata"],
|
||||
type: json["type"],
|
||||
metadata: json['metadata'],
|
||||
type: json['type'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"account_id": accountId,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt?.toIso8601String(),
|
||||
"metadata": metadata,
|
||||
"type": type,
|
||||
'id': id,
|
||||
'account_id': accountId,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
'deleted_at': deletedAt?.toIso8601String(),
|
||||
'metadata': metadata,
|
||||
'type': type,
|
||||
};
|
||||
}
|
||||
|
@ -38,40 +38,40 @@ class Attachment {
|
||||
});
|
||||
|
||||
factory Attachment.fromJson(Map<String, dynamic> json) => Attachment(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"],
|
||||
uuid: json["uuid"],
|
||||
size: json["size"],
|
||||
name: json["name"],
|
||||
alt: json["alt"],
|
||||
usage: json["usage"],
|
||||
mimetype: json["mimetype"],
|
||||
hash: json["hash"],
|
||||
destination: json["destination"],
|
||||
metadata: json["metadata"],
|
||||
isMature: json["is_mature"],
|
||||
account: Account.fromJson(json["account"]),
|
||||
accountId: json["account_id"],
|
||||
id: json['id'],
|
||||
createdAt: DateTime.parse(json['created_at']),
|
||||
updatedAt: DateTime.parse(json['updated_at']),
|
||||
deletedAt: json['deleted_at'],
|
||||
uuid: json['uuid'],
|
||||
size: json['size'],
|
||||
name: json['name'],
|
||||
alt: json['alt'],
|
||||
usage: json['usage'],
|
||||
mimetype: json['mimetype'],
|
||||
hash: json['hash'],
|
||||
destination: json['destination'],
|
||||
metadata: json['metadata'],
|
||||
isMature: json['is_mature'],
|
||||
account: Account.fromJson(json['account']),
|
||||
accountId: json['account_id'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"uuid": uuid,
|
||||
"size": size,
|
||||
"name": name,
|
||||
"alt": alt,
|
||||
"usage": usage,
|
||||
"mimetype": mimetype,
|
||||
"hash": hash,
|
||||
"destination": destination,
|
||||
"metadata": metadata,
|
||||
"is_mature": isMature,
|
||||
"account": account.toJson(),
|
||||
"account_id": accountId,
|
||||
'id': id,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
'deleted_at': deletedAt,
|
||||
'uuid': uuid,
|
||||
'size': size,
|
||||
'name': name,
|
||||
'alt': alt,
|
||||
'usage': usage,
|
||||
'mimetype': mimetype,
|
||||
'hash': hash,
|
||||
'destination': destination,
|
||||
'metadata': metadata,
|
||||
'is_mature': isMature,
|
||||
'account': account.toJson(),
|
||||
'account_id': accountId,
|
||||
};
|
||||
}
|
@ -44,8 +44,8 @@ class Message {
|
||||
deletedAt: json['deleted_at'],
|
||||
content: json['content'],
|
||||
type: json['type'],
|
||||
attachments: json["attachments"] != null
|
||||
? List<int>.from(json["attachments"])
|
||||
attachments: json['attachments'] != null
|
||||
? List<int>.from(json['attachments'])
|
||||
: null,
|
||||
channel:
|
||||
json['channel'] != null ? Channel.fromJson(json['channel']) : null,
|
||||
|
@ -53,36 +53,36 @@ class Post {
|
||||
});
|
||||
|
||||
factory Post.fromJson(Map<String, dynamic> json) => Post(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.parse(json["created_at"]),
|
||||
updatedAt: DateTime.parse(json["updated_at"]),
|
||||
deletedAt: json["deleted_at"] != null
|
||||
id: json['id'],
|
||||
createdAt: DateTime.parse(json['created_at']),
|
||||
updatedAt: DateTime.parse(json['updated_at']),
|
||||
deletedAt: json['deleted_at'] != null
|
||||
? DateTime.parse(json['deleted_at'])
|
||||
: null,
|
||||
alias: json["alias"],
|
||||
content: json["content"],
|
||||
tags: json["tags"],
|
||||
categories: json["categories"],
|
||||
reactions: json["reactions"],
|
||||
replies: json["replies"],
|
||||
attachments: json["attachments"] != null
|
||||
? List<int>.from(json["attachments"])
|
||||
alias: json['alias'],
|
||||
content: json['content'],
|
||||
tags: json['tags'],
|
||||
categories: json['categories'],
|
||||
reactions: json['reactions'],
|
||||
replies: json['replies'],
|
||||
attachments: json['attachments'] != null
|
||||
? List<int>.from(json['attachments'])
|
||||
: null,
|
||||
replyId: json["reply_id"],
|
||||
repostId: json["repost_id"],
|
||||
realmId: json["realm_id"],
|
||||
replyId: json['reply_id'],
|
||||
repostId: json['repost_id'],
|
||||
realmId: json['realm_id'],
|
||||
replyTo:
|
||||
json["reply_to"] != null ? Post.fromJson(json["reply_to"]) : null,
|
||||
json['reply_to'] != null ? Post.fromJson(json['reply_to']) : null,
|
||||
repostTo:
|
||||
json["repost_to"] != null ? Post.fromJson(json["repost_to"]) : null,
|
||||
realm: json["realm"] != null ? Realm.fromJson(json["realm"]) : null,
|
||||
publishedAt: json["published_at"] != null ? DateTime.parse(json["published_at"]) : null,
|
||||
authorId: json["author_id"],
|
||||
author: Account.fromJson(json["author"]),
|
||||
replyCount: json["reply_count"],
|
||||
reactionCount: json["reaction_count"],
|
||||
reactionList: json["reaction_list"] != null
|
||||
? json["reaction_list"]
|
||||
json['repost_to'] != null ? Post.fromJson(json['repost_to']) : null,
|
||||
realm: json['realm'] != null ? Realm.fromJson(json['realm']) : null,
|
||||
publishedAt: json['published_at'] != null ? DateTime.parse(json['published_at']) : null,
|
||||
authorId: json['author_id'],
|
||||
author: Account.fromJson(json['author']),
|
||||
replyCount: json['reply_count'],
|
||||
reactionCount: json['reaction_count'],
|
||||
reactionList: json['reaction_list'] != null
|
||||
? json['reaction_list']
|
||||
.map((key, value) => MapEntry(
|
||||
key,
|
||||
int.tryParse(value.toString()) ??
|
||||
@ -92,28 +92,28 @@ class Post {
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"created_at": createdAt.toIso8601String(),
|
||||
"updated_at": updatedAt.toIso8601String(),
|
||||
"deleted_at": deletedAt,
|
||||
"alias": alias,
|
||||
"content": content,
|
||||
"tags": tags,
|
||||
"categories": categories,
|
||||
"reactions": reactions,
|
||||
"replies": replies,
|
||||
"attachments": attachments,
|
||||
"reply_id": replyId,
|
||||
"repost_id": repostId,
|
||||
"realm_id": realmId,
|
||||
"reply_to": replyTo?.toJson(),
|
||||
"repost_to": repostTo?.toJson(),
|
||||
"realm": realm?.toJson(),
|
||||
"published_at": publishedAt?.toIso8601String(),
|
||||
"author_id": authorId,
|
||||
"author": author.toJson(),
|
||||
"reply_count": replyCount,
|
||||
"reaction_count": reactionCount,
|
||||
"reaction_list": reactionList,
|
||||
'id': id,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
'deleted_at': deletedAt,
|
||||
'alias': alias,
|
||||
'content': content,
|
||||
'tags': tags,
|
||||
'categories': categories,
|
||||
'reactions': reactions,
|
||||
'replies': replies,
|
||||
'attachments': attachments,
|
||||
'reply_id': replyId,
|
||||
'repost_id': repostId,
|
||||
'realm_id': realmId,
|
||||
'reply_to': replyTo?.toJson(),
|
||||
'repost_to': repostTo?.toJson(),
|
||||
'realm': realm?.toJson(),
|
||||
'published_at': publishedAt?.toIso8601String(),
|
||||
'author_id': authorId,
|
||||
'author': author.toJson(),
|
||||
'reply_count': replyCount,
|
||||
'reaction_count': reactionCount,
|
||||
'reaction_list': reactionList,
|
||||
};
|
||||
}
|
||||
|
@ -190,10 +190,10 @@ class AccountProvider extends GetxController {
|
||||
}
|
||||
|
||||
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
|
||||
provider = "apple";
|
||||
provider = 'apple';
|
||||
token = await FirebaseMessaging.instance.getAPNSToken();
|
||||
} else {
|
||||
provider = "firebase";
|
||||
provider = 'firebase';
|
||||
token = await FirebaseMessaging.instance.getToken();
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@ class AuthProvider extends GetConnect {
|
||||
|
||||
if (credentials!.isExpired) {
|
||||
await refreshCredentials();
|
||||
log("Refreshed credentials at ${DateTime.now()}");
|
||||
log('Refreshed credentials at ${DateTime.now()}');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,19 +13,23 @@ Future<MessageHistoryDb> createHistoryDb() async {
|
||||
|
||||
extension MessageHistoryHelper on MessageHistoryDb {
|
||||
receiveMessage(Message remote) async {
|
||||
await localMessages.insert(LocalMessage(
|
||||
final entry = LocalMessage(
|
||||
remote.id,
|
||||
remote,
|
||||
remote.channelId,
|
||||
));
|
||||
);
|
||||
await localMessages.insert(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
replaceMessage(Message remote) async {
|
||||
await localMessages.update(LocalMessage(
|
||||
final entry = LocalMessage(
|
||||
remote.id,
|
||||
remote,
|
||||
remote.channelId,
|
||||
));
|
||||
);
|
||||
await localMessages.update(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
burnMessage(int id) async {
|
||||
@ -38,18 +42,22 @@ extension MessageHistoryHelper on MessageHistoryDb {
|
||||
final data = await _getRemoteMessages(
|
||||
channel,
|
||||
scope,
|
||||
remainBreath: 3,
|
||||
remainBreath: 10,
|
||||
offset: offset,
|
||||
onBrake: (items) {
|
||||
return items.any((x) => x.id == lastOne?.id);
|
||||
},
|
||||
);
|
||||
await localMessages.insertBulk(
|
||||
data.map((x) => LocalMessage(x.id, x, x.channelId)).toList(),
|
||||
);
|
||||
if (data != null) {
|
||||
await localMessages.insertBulk(
|
||||
data.$1.map((x) => LocalMessage(x.id, x, x.channelId)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
return data?.$2 ?? 0;
|
||||
}
|
||||
|
||||
Future<List<Message>> _getRemoteMessages(
|
||||
Future<(List<Message>, int)?> _getRemoteMessages(
|
||||
Channel channel,
|
||||
String scope, {
|
||||
required int remainBreath,
|
||||
@ -58,11 +66,11 @@ extension MessageHistoryHelper on MessageHistoryDb {
|
||||
offset = 0,
|
||||
}) async {
|
||||
if (remainBreath <= 0) {
|
||||
return List.empty();
|
||||
return null;
|
||||
}
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
if (!await auth.isAuthorized) return List.empty();
|
||||
if (!await auth.isAuthorized) return null;
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
|
||||
@ -78,18 +86,20 @@ extension MessageHistoryHelper on MessageHistoryDb {
|
||||
response.data?.map((e) => Message.fromJson(e)).toList() ?? List.empty();
|
||||
|
||||
if (onBrake != null && onBrake(result)) {
|
||||
return result;
|
||||
return (result, response.count);
|
||||
}
|
||||
|
||||
final expandResult = await _getRemoteMessages(
|
||||
channel,
|
||||
scope,
|
||||
remainBreath: remainBreath - 1,
|
||||
take: take,
|
||||
offset: offset + result.length,
|
||||
);
|
||||
final expandResult = (await _getRemoteMessages(
|
||||
channel,
|
||||
scope,
|
||||
remainBreath: remainBreath - 1,
|
||||
take: take,
|
||||
offset: offset + result.length,
|
||||
))
|
||||
?.$1 ??
|
||||
List.empty();
|
||||
|
||||
return [...result, ...expandResult];
|
||||
return ([...result, ...expandResult], response.count);
|
||||
}
|
||||
|
||||
Future<List<LocalMessage>> listMessages(Channel channel) async {
|
||||
|
@ -27,9 +27,9 @@ class _CallScreenState extends State<CallScreen> {
|
||||
DateTime.now().difference(provider.current.value!.createdAt);
|
||||
|
||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
String formattedTime = "${twoDigits(duration.inHours)}:"
|
||||
"${twoDigits(duration.inMinutes.remainder(60))}:"
|
||||
"${twoDigits(duration.inSeconds.remainder(60))}";
|
||||
String formattedTime = '${twoDigits(duration.inHours)}:'
|
||||
'${twoDigits(duration.inMinutes.remainder(60))}:'
|
||||
'${twoDigits(duration.inSeconds.remainder(60))}';
|
||||
|
||||
return formattedTime;
|
||||
}
|
||||
@ -66,7 +66,7 @@ class _CallScreenState extends State<CallScreen> {
|
||||
text: 'call'.tr,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const TextSpan(text: "\n"),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: currentDuration,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
|
@ -2,8 +2,8 @@ import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/controllers/chat_history_controller.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/call.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
@ -13,8 +13,6 @@ import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/chat.dart';
|
||||
import 'package:solian/providers/content/call.dart';
|
||||
import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/providers/message/helper.dart';
|
||||
import 'package:solian/providers/message/history.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/channel/channel_detail.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
@ -41,10 +39,7 @@ class ChannelChatScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final _chatScrollController = ScrollController();
|
||||
|
||||
bool _isBusy = false;
|
||||
bool _isLoadingMore = false;
|
||||
int? _accountId;
|
||||
|
||||
String? _overrideAlias;
|
||||
@ -54,9 +49,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
ChannelMember? _channelProfile;
|
||||
StreamSubscription<NetworkPackage>? _subscription;
|
||||
|
||||
int _nextHistorySyncOffset = 0;
|
||||
MessageHistoryDb? _db;
|
||||
List<LocalMessage> _currentHistory = List.empty();
|
||||
late final ChatHistoryController _chatController;
|
||||
|
||||
getProfile() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
@ -111,22 +104,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> getMessages() async {
|
||||
await _db!.syncMessages(
|
||||
_channel!,
|
||||
scope: widget.realm,
|
||||
offset: _nextHistorySyncOffset,
|
||||
);
|
||||
await syncHistory();
|
||||
}
|
||||
|
||||
Future<void> syncHistory() async {
|
||||
final data = await _db!.localMessages.findAllByChannel(_channel!.id);
|
||||
setState(() {
|
||||
_currentHistory = data;
|
||||
});
|
||||
}
|
||||
|
||||
void listenMessages() {
|
||||
final ChatProvider provider = Get.find();
|
||||
_subscription = provider.stream.stream.listen((event) {
|
||||
@ -134,19 +111,19 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
case 'messages.new':
|
||||
final payload = Message.fromJson(event.payload!);
|
||||
if (payload.channelId == _channel?.id) {
|
||||
_db?.receiveMessage(payload);
|
||||
_chatController.receiveMessage(payload);
|
||||
}
|
||||
break;
|
||||
case 'messages.update':
|
||||
final payload = Message.fromJson(event.payload!);
|
||||
if (payload.channelId == _channel?.id) {
|
||||
_db?.replaceMessage(payload);
|
||||
_chatController.replaceMessage(payload);
|
||||
}
|
||||
break;
|
||||
case 'messages.burnt':
|
||||
final payload = Message.fromJson(event.payload!);
|
||||
if (payload.channelId == _channel?.id) {
|
||||
_db?.burnMessage(payload.id);
|
||||
_chatController.burnMessage(payload.id);
|
||||
}
|
||||
break;
|
||||
case 'calls.new':
|
||||
@ -157,7 +134,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
_ongoingCall = null;
|
||||
break;
|
||||
}
|
||||
syncHistory();
|
||||
});
|
||||
}
|
||||
|
||||
@ -211,18 +187,18 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
bool isMerged = false, hasMerged = false;
|
||||
if (index > 0) {
|
||||
hasMerged = checkMessageMergeable(
|
||||
_currentHistory[index - 1].data,
|
||||
_currentHistory[index].data,
|
||||
_chatController.currentHistory[index - 1].data,
|
||||
_chatController.currentHistory[index].data,
|
||||
);
|
||||
}
|
||||
if (index + 1 < _currentHistory.length) {
|
||||
if (index + 1 < _chatController.currentHistory.length) {
|
||||
isMerged = checkMessageMergeable(
|
||||
_currentHistory[index].data,
|
||||
_currentHistory[index + 1].data,
|
||||
_chatController.currentHistory[index].data,
|
||||
_chatController.currentHistory[index + 1].data,
|
||||
);
|
||||
}
|
||||
|
||||
final item = _currentHistory[index].data;
|
||||
final item = _chatController.currentHistory[index].data;
|
||||
|
||||
return InkWell(
|
||||
child: Container(
|
||||
@ -253,28 +229,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_chatScrollController.addListener(() async {
|
||||
if (_chatScrollController.position.pixels ==
|
||||
_chatScrollController.position.maxScrollExtent) {
|
||||
setState(() => _isLoadingMore = true);
|
||||
_nextHistorySyncOffset = _currentHistory.length;
|
||||
await getMessages();
|
||||
setState(() => _isLoadingMore = false);
|
||||
}
|
||||
_chatController = ChatHistoryController();
|
||||
_chatController.initialize();
|
||||
|
||||
getChannel().then((_) {
|
||||
_chatController.getMessages(_channel!, widget.realm);
|
||||
});
|
||||
|
||||
createHistoryDb().then((db) async {
|
||||
_db = db;
|
||||
getProfile();
|
||||
getOngoingCall();
|
||||
|
||||
await getChannel();
|
||||
await syncHistory();
|
||||
|
||||
getProfile();
|
||||
getOngoingCall();
|
||||
getMessages();
|
||||
|
||||
listenMessages();
|
||||
});
|
||||
listenMessages();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
@ -352,52 +317,66 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
if (_isLoadingMore)
|
||||
const LinearProgressIndicator()
|
||||
.paddingOnly(bottom: 4)
|
||||
.animate()
|
||||
.slideY(),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: _chatScrollController,
|
||||
itemCount: _currentHistory.length,
|
||||
clipBehavior: Clip.none,
|
||||
child: CustomScrollView(
|
||||
reverse: true,
|
||||
itemBuilder: buildHistory,
|
||||
).paddingOnly(bottom: 56),
|
||||
slivers: [
|
||||
Obx(() {
|
||||
return SliverList.builder(
|
||||
itemCount: _chatController.currentHistory.length,
|
||||
itemBuilder: buildHistory,
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
final amount = _chatController.totalHistoryCount -
|
||||
_chatController.currentHistory.length;
|
||||
if (amount > 0) {
|
||||
return SliverToBoxAdapter(
|
||||
child: ListTile(
|
||||
tileColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerLow,
|
||||
leading: const Icon(Icons.sync_disabled),
|
||||
title: Text('messageUnsync'.tr),
|
||||
subtitle: Text('messageUnsyncCaption'.trParams({
|
||||
'count': amount.string,
|
||||
})),
|
||||
onTap: () {},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SliverToBoxAdapter(child: SizedBox());
|
||||
}
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
||||
child: SafeArea(
|
||||
child: ChatMessageInput(
|
||||
edit: _messageToEditing,
|
||||
reply: _messageToReplying,
|
||||
realm: widget.realm,
|
||||
placeholder: placeholder,
|
||||
channel: _channel!,
|
||||
onSent: (Message item) {
|
||||
setState(() {
|
||||
_db?.receiveMessage(item);
|
||||
syncHistory();
|
||||
});
|
||||
},
|
||||
onReset: () {
|
||||
setState(() {
|
||||
_messageToReplying = null;
|
||||
_messageToEditing = null;
|
||||
});
|
||||
},
|
||||
ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
||||
child: SafeArea(
|
||||
child: ChatMessageInput(
|
||||
edit: _messageToEditing,
|
||||
reply: _messageToReplying,
|
||||
realm: widget.realm,
|
||||
placeholder: placeholder,
|
||||
channel: _channel!,
|
||||
onSent: (Message item) {
|
||||
setState(() {
|
||||
_chatController.receiveMessage(item);
|
||||
});
|
||||
},
|
||||
onReset: () {
|
||||
setState(() {
|
||||
_messageToReplying = null;
|
||||
_messageToEditing = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_ongoingCall != null)
|
||||
Positioned(
|
||||
|
@ -164,6 +164,8 @@ class SolianMessages extends Translations {
|
||||
'channelNotifyLevelMentioned': 'Only mentioned',
|
||||
'channelNotifyLevelNone': 'Ignore all',
|
||||
'channelNotifyLevelApplied': 'Your notification settings has been applied.',
|
||||
'messageUnsync': 'Messages Un-synced',
|
||||
'messageUnsyncCaption': '@count message(s) still in un-synced.',
|
||||
'messageDecoding': 'Decoding...',
|
||||
'messageDecodeFailed': 'Unable to decode: @message',
|
||||
'messageInputPlaceholder': 'Message @channel',
|
||||
@ -364,6 +366,8 @@ class SolianMessages extends Translations {
|
||||
'channelNotifyLevelMentioned': '仅提及',
|
||||
'channelNotifyLevelNone': '忽略一切',
|
||||
'channelNotifyLevelApplied': '你的通知设置已经应用。',
|
||||
'messageUnsync': '消息未同步',
|
||||
'messageUnsyncCaption': '还有 @count 条消息未同步',
|
||||
'messageDecoding': '解码信息中…',
|
||||
'messageDecodeFailed': '解码信息失败:@message',
|
||||
'messageInputPlaceholder': '在 @channel 发信息',
|
||||
|
@ -67,7 +67,7 @@ class SliverFriendList extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return SliverList.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (_, __) => buildItem(_, __),
|
||||
itemBuilder: (context, idx) => buildItem(context, idx),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ class _ChatCallButtonState extends State<ChatCallButton> {
|
||||
? widget.realm?.alias
|
||||
: 'global';
|
||||
final resp = await client
|
||||
.delete('/api/channels/${scope}/${widget.channel.alias}/calls/ongoing');
|
||||
.delete('/api/channels/$scope/${widget.channel.alias}/calls/ongoing');
|
||||
if (resp.statusCode == 200) {
|
||||
if (widget.onEnded != null) widget.onEnded!();
|
||||
} else {
|
||||
|
@ -98,7 +98,7 @@ class _PostItemState extends State<PostItem> {
|
||||
|
||||
if (labels.isNotEmpty) {
|
||||
return Text(
|
||||
labels.join(" · "),
|
||||
labels.join(' · '),
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
|
Loading…
Reference in New Issue
Block a user