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