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 = static const String sessionMsg =
'https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs'; '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 // timezone_offset=-480
// id=849312409672744983 // id=849312409672744983

View File

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

View File

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

View File

@ -6,10 +6,13 @@ import 'package:pilipala/models/msg/session.dart';
class WhisperController extends GetxController { class WhisperController extends GetxController {
RxList<SessionList> sessionList = <SessionList>[].obs; RxList<SessionList> sessionList = <SessionList>[].obs;
RxList<AccountListModel> accountList = <AccountListModel>[].obs; RxList<AccountListModel> accountList = <AccountListModel>[].obs;
bool isLoading = false;
Future querySessionList() async { Future querySessionList(String? type) async {
var res = await MsgHttp.sessionList(); if (isLoading) return;
if (res['data'].sessionList.isNotEmpty) { 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); await queryAccountList(res['data'].sessionList);
// 将 accountList 转换为 Map 结构 // 将 accountList 转换为 Map 结构
Map<int, dynamic> accountMap = {}; Map<int, dynamic> accountMap = {};
@ -32,10 +35,14 @@ class WhisperController extends GetxController {
} }
} }
} }
if (res['status']) { if (res['status'] && res['data'].sessionList != null) {
sessionList.value = res['data'].sessionList; if (type == 'onLoad') {
sessionList.addAll(res['data'].sessionList);
} else {
sessionList.value = res['data'].sessionList;
}
} }
isLoading = false;
return res; return res;
} }
@ -47,4 +54,12 @@ class WhisperController extends GetxController {
} }
return res; 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:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -16,18 +17,31 @@ class _WhisperPageState extends State<WhisperPage> {
late final WhisperController _whisperController = late final WhisperController _whisperController =
Get.put(WhisperController()); Get.put(WhisperController());
late Future _futureBuilderFuture; late Future _futureBuilderFuture;
final ScrollController _scrollController = ScrollController();
@override @override
void initState() { void initState() {
super.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
scrolledUnderElevation: 0, title: const Text('消息'),
), ),
body: Column( body: Column(
children: [ children: [
@ -82,95 +96,133 @@ class _WhisperPageState extends State<WhisperPage> {
// }, // },
// ), // ),
Expanded( Expanded(
child: FutureBuilder( child: RefreshIndicator(
future: _futureBuilderFuture, onRefresh: () async {
builder: (context, snapshot) { await _whisperController.onRefresh();
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
List sessionList = _whisperController.sessionList;
return Obx(
() => sessionList.isEmpty
? const SizedBox()
: ListView.builder(
itemCount: sessionList.length,
shrinkWrap: true,
itemBuilder: (_, int i) {
return ListTile(
onTap: () {
Get.toNamed(
'/whisperDetail?talkerId=${sessionList[i].talkerId}&name=${sessionList[i].accountInfo.name}');
},
leading: NetworkImgLayer(
width: 45,
height: 45,
type: 'avatar',
src: sessionList[i].accountInfo.face,
),
title: Text(
sessionList[i].accountInfo.name,
style:
Theme.of(context).textTheme.titleSmall,
),
subtitle: Text(
sessionList[i].lastMsg.content['text'] ??
sessionList[i]
.lastMsg
.content['content'] ??
sessionList[i]
.lastMsg
.content['title'],
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.labelMedium!
.copyWith(
color: Theme.of(context)
.colorScheme
.outline)),
trailing: Text(
Utils.dateFormat(
sessionList[i].lastMsg.timestamp),
style: Theme.of(context)
.textTheme
.labelSmall!
.copyWith(
color: Theme.of(context)
.colorScheme
.outline),
),
);
},
),
);
} else {
// 请求错误
return SizedBox();
}
} else {
// 骨架屏
return SizedBox();
}
}, },
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: [
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
List sessionList = _whisperController.sessionList;
return Obx(
() => sessionList.isEmpty
? const SizedBox()
: ListView.separated(
itemCount: sessionList.length,
shrinkWrap: true,
physics:
const NeverScrollableScrollPhysics(),
itemBuilder: (_, int i) {
return ListTile(
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: 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,
),
),
title: Text(
sessionList[i].accountInfo.name),
subtitle: Text(
sessionList[i]
.lastMsg
.content['text'] ??
sessionList[i]
.lastMsg
.content['content'] ??
sessionList[i]
.lastMsg
.content['title'] ??
sessionList[i]
.lastMsg
.content[
'reply_content'] ??
'',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.labelMedium!
.copyWith(
color: Theme.of(context)
.colorScheme
.outline)),
trailing: Text(
Utils.dateFormat(sessionList[i]
.lastMsg
.timestamp),
style: Theme.of(context)
.textTheme
.labelSmall!
.copyWith(
color: Theme.of(context)
.colorScheme
.outline),
),
);
},
separatorBuilder:
(BuildContext context, int index) {
return Divider(
indent: 72,
endIndent: 20,
height: 6,
color: Colors.grey.withOpacity(0.1),
);
},
),
);
} else {
// 请求错误
return SizedBox();
}
} else {
// 骨架屏
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 { class WhisperDetailController extends GetxController {
late int talkerId; late int talkerId;
RxString name = ''.obs; late String name;
late String face;
late String mid;
RxList<MessageItem> messageList = <MessageItem>[].obs; RxList<MessageItem> messageList = <MessageItem>[].obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
talkerId = int.parse(Get.parameters['talkerId']!); 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 { Future querySessionMsg() async {

View File

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

View File

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