feat: 消息分页
This commit is contained in:
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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'];
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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('现在'),
|
||||
// )
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
],
|
||||
),
|
||||
|
@ -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'],
|
||||
|
Reference in New Issue
Block a user