feat: 消息分页

This commit is contained in:
guozhigq
2023-12-17 14:55:52 +08:00
parent a43c071eb5
commit a6ab72cadd
8 changed files with 259 additions and 144 deletions

View File

@ -359,6 +359,18 @@ class Api {
static const String sessionMsg =
'https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs';
/// 标记已读 POST
/// talker_id:
/// session_type: 1
/// ack_seqno: 920224140918926
/// build: 0
/// mobi_app: web
/// csrf_token:
/// csrf:
static const String updateAck =
'https://api.vc.bilibili.com/session_svr/v1/session_svr/update_ack';
// 获取某个动态详情
// timezone_offset=-480
// id=849312409672744983

View File

@ -6,16 +6,21 @@ import 'package:pilipala/utils/wbi_sign.dart';
class MsgHttp {
// 会话列表
static Future sessionList() async {
Map params = await WbiSign().makSign({
static Future sessionList({int? endTs}) async {
Map<String, dynamic> params = {
'session_type': 1,
'group_fold': 1,
'unfollow_fold': 0,
'sort_rule': 2,
'build': 0,
'mobi_app': 'web',
});
var res = await Request().get(Api.sessionList, data: params);
};
if (endTs != null) {
params['end_ts'] = endTs;
}
Map signParams = await WbiSign().makSign(params);
var res = await Request().get(Api.sessionList, data: signParams);
if (res.data['code'] == 0) {
return {
'status': true,

View File

@ -13,7 +13,7 @@ class SessionDataModel {
SessionDataModel.fromJson(Map<String, dynamic> json) {
sessionList = json['session_list']
.map<SessionList>((e) => SessionList.fromJson(e))
?.map<SessionList>((e) => SessionList.fromJson(e))
.toList();
hasMore = json['has_more'];
}

View File

@ -6,10 +6,13 @@ import 'package:pilipala/models/msg/session.dart';
class WhisperController extends GetxController {
RxList<SessionList> sessionList = <SessionList>[].obs;
RxList<AccountListModel> accountList = <AccountListModel>[].obs;
bool isLoading = false;
Future querySessionList() async {
var res = await MsgHttp.sessionList();
if (res['data'].sessionList.isNotEmpty) {
Future querySessionList(String? type) async {
if (isLoading) return;
var res = await MsgHttp.sessionList(
endTs: type == 'onLoad' ? sessionList.last.sessionTs : null);
if (res['data'].sessionList != null && res['data'].sessionList.isNotEmpty) {
await queryAccountList(res['data'].sessionList);
// 将 accountList 转换为 Map 结构
Map<int, dynamic> accountMap = {};
@ -32,10 +35,14 @@ class WhisperController extends GetxController {
}
}
}
if (res['status']) {
if (res['status'] && res['data'].sessionList != null) {
if (type == 'onLoad') {
sessionList.addAll(res['data'].sessionList);
} else {
sessionList.value = res['data'].sessionList;
}
}
isLoading = false;
return res;
}
@ -47,4 +54,12 @@ class WhisperController extends GetxController {
}
return res;
}
Future onLoad() async {
querySessionList('onLoad');
}
Future onRefresh() async {
querySessionList('onRefresh');
}
}

View File

@ -1,3 +1,4 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -16,18 +17,31 @@ class _WhisperPageState extends State<WhisperPage> {
late final WhisperController _whisperController =
Get.put(WhisperController());
late Future _futureBuilderFuture;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_futureBuilderFuture = _whisperController.querySessionList();
_futureBuilderFuture = _whisperController.querySessionList('init');
_scrollController.addListener(_scrollListener);
}
Future _scrollListener() async {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('my-throttler', const Duration(milliseconds: 800),
() async {
await _whisperController.onLoad();
_whisperController.isLoading = true;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
scrolledUnderElevation: 0,
title: const Text('消息'),
),
body: Column(
children: [
@ -82,7 +96,15 @@ class _WhisperPageState extends State<WhisperPage> {
// },
// ),
Expanded(
child: FutureBuilder(
child: RefreshIndicator(
onRefresh: () async {
await _whisperController.onRefresh();
},
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: [
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
@ -92,34 +114,66 @@ class _WhisperPageState extends State<WhisperPage> {
return Obx(
() => sessionList.isEmpty
? const SizedBox()
: ListView.builder(
: ListView.separated(
itemCount: sessionList.length,
shrinkWrap: true,
physics:
const NeverScrollableScrollPhysics(),
itemBuilder: (_, int i) {
return ListTile(
onTap: () {
Get.toNamed(
'/whisperDetail?talkerId=${sessionList[i].talkerId}&name=${sessionList[i].accountInfo.name}');
onTap: () => Get.toNamed(
'/whisperDetail',
parameters: {
'talkerId': sessionList[i]
.talkerId
.toString(),
'name': sessionList[i]
.accountInfo
.name,
'face': sessionList[i]
.accountInfo
.face,
'mid': sessionList[i]
.accountInfo
.mid
.toString(),
},
leading: NetworkImgLayer(
),
leading: Badge(
isLabelVisible: false,
backgroundColor: Theme.of(context)
.colorScheme
.primary,
label: Text(sessionList[i]
.unreadCount
.toString()),
alignment: Alignment.bottomRight,
child: NetworkImgLayer(
width: 45,
height: 45,
type: 'avatar',
src: sessionList[i].accountInfo.face,
src: sessionList[i]
.accountInfo
.face,
),
),
title: Text(
sessionList[i].accountInfo.name,
style:
Theme.of(context).textTheme.titleSmall,
),
sessionList[i].accountInfo.name),
subtitle: Text(
sessionList[i].lastMsg.content['text'] ??
sessionList[i]
.lastMsg
.content['text'] ??
sessionList[i]
.lastMsg
.content['content'] ??
sessionList[i]
.lastMsg
.content['title'],
.content['title'] ??
sessionList[i]
.lastMsg
.content[
'reply_content'] ??
'',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
@ -130,8 +184,9 @@ class _WhisperPageState extends State<WhisperPage> {
.colorScheme
.outline)),
trailing: Text(
Utils.dateFormat(
sessionList[i].lastMsg.timestamp),
Utils.dateFormat(sessionList[i]
.lastMsg
.timestamp),
style: Theme.of(context)
.textTheme
.labelSmall!
@ -142,6 +197,15 @@ class _WhisperPageState extends State<WhisperPage> {
),
);
},
separatorBuilder:
(BuildContext context, int index) {
return Divider(
indent: 72,
endIndent: 20,
height: 6,
color: Colors.grey.withOpacity(0.1),
);
},
),
);
} else {
@ -153,24 +217,12 @@ class _WhisperPageState extends State<WhisperPage> {
return SizedBox();
}
},
)
],
),
),
),
),
// ListTile(
// onTap: () {},
// leading: CircleAvatar(),
// title: Text('钱瑞昌'),
// subtitle: Text('没事', style: Theme.of(context).textTheme.bodySmall),
// trailing: Text('昨天'),
// ),
// ListTile(
// onTap: () {},
// leading: CircleAvatar(),
// title: Text('李天'),
// subtitle:
// Text('明天有空吗', style: Theme.of(context).textTheme.bodySmall),
// trailing: Text('现在'),
// )
],
),
);

View File

@ -4,14 +4,18 @@ import 'package:pilipala/models/msg/session.dart';
class WhisperDetailController extends GetxController {
late int talkerId;
RxString name = ''.obs;
late String name;
late String face;
late String mid;
RxList<MessageItem> messageList = <MessageItem>[].obs;
@override
void onInit() {
super.onInit();
talkerId = int.parse(Get.parameters['talkerId']!);
name.value = Get.parameters['name']!;
name = Get.parameters['name']!;
face = Get.parameters['face']!;
mid = Get.parameters['mid']!;
}
Future querySessionMsg() async {

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/whisperDetail/controller.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'widget/chat_item.dart';
@ -27,7 +29,6 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
scrolledUnderElevation: 0,
title: SizedBox(
width: double.infinity,
height: 50,
@ -52,15 +53,36 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
icon: Icon(
Icons.arrow_back_outlined,
size: 18,
color: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
Obx(
() => Text(
_whisperDetailController.name.value,
GestureDetector(
onTap: () {
feedBack();
Get.toNamed(
'/member?mid=${_whisperDetailController.mid}',
arguments: {
'face': _whisperDetailController.face,
'heroTag': null
},
);
},
child: Row(
children: [
NetworkImgLayer(
width: 34,
height: 34,
type: 'avatar',
src: _whisperDetailController.face,
),
const SizedBox(width: 6),
Text(
_whisperDetailController.name,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
),
const SizedBox(width: 36, height: 36),
],
@ -105,12 +127,14 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
}
},
),
// resizeToAvoidBottomInset: true,
bottomNavigationBar: Container(
width: double.infinity,
height: MediaQuery.of(context).padding.bottom + 70,
padding: EdgeInsets.only(
left: 8,
right: 12,
top: 12,
bottom: MediaQuery.of(context).padding.bottom,
),
decoration: BoxDecoration(
@ -124,6 +148,7 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// IconButton(
// onPressed: () {},
@ -139,25 +164,26 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
color: Theme.of(context).colorScheme.outline,
),
),
// Expanded(
// child: Container(
// height: 42,
// decoration: BoxDecoration(
// color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
// borderRadius: BorderRadius.circular(40.0),
// ),
// child: TextField(
// readOnly: true,
// style: Theme.of(context).textTheme.titleMedium,
// decoration: const InputDecoration(
// border: InputBorder.none, // 移除默认边框
// hintText: '请输入内容', // 提示文本
// contentPadding: EdgeInsets.symmetric(
// horizontal: 12.0, vertical: 12.0), // 内边距
// ),
// ),
// ),
// ),
Expanded(
child: Container(
height: 45,
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.primary.withOpacity(0.08),
borderRadius: BorderRadius.circular(40.0),
),
child: TextField(
readOnly: true,
style: Theme.of(context).textTheme.titleMedium,
decoration: const InputDecoration(
border: InputBorder.none, // 移除默认边框
hintText: '开发中 ...', // 提示文本
contentPadding: EdgeInsets.symmetric(
horizontal: 16.0, vertical: 12.0), // 内边距
),
),
),
),
const SizedBox(width: 16),
],
),

View File

@ -17,6 +17,9 @@ class ChatItem extends StatelessWidget {
bool isOwner = item.senderUid == 17340771;
bool isPic = item.msgType == 2;
bool isText = item.msgType == 1;
bool isAchive = item.msgType == 11;
bool isArticle = item.msgType == 12;
bool isSystem =
item.msgType == 18 || item.msgType == 10 || item.msgType == 13;
int msgType = item.msgType;
@ -115,7 +118,10 @@ class SystemNotice extends StatelessWidget {
maxWidth: 300.0, // 设置最大宽度为200.0
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withOpacity(0.4),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
@ -127,9 +133,6 @@ class SystemNotice extends StatelessWidget {
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(content['title'],
style: Theme.of(context)
@ -142,11 +145,9 @@ class SystemNotice extends StatelessWidget {
.textTheme
.labelSmall!
.copyWith(color: Theme.of(context).colorScheme.outline),
)
],
),
Divider(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
color: Theme.of(context).colorScheme.primary.withOpacity(0.05),
),
Text(
content['text'],