feat(mobile-next): 通讯录按部门分组并补齐职务行与申请窄屏
Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
7f131457f7
commit
49b346d375
@@ -4,18 +4,45 @@ import 'package:openim/pages/contacts/group_profile_panel/group_profile_panel_lo
|
|||||||
import 'package:openim/routes/app_navigator.dart';
|
import 'package:openim/routes/app_navigator.dart';
|
||||||
import 'package:openim_common/openim_common.dart';
|
import 'package:openim_common/openim_common.dart';
|
||||||
|
|
||||||
|
import '../../company/directory/directory_api.dart';
|
||||||
import '../../core/controller/im_controller.dart';
|
import '../../core/controller/im_controller.dart';
|
||||||
import '../home/home_logic.dart';
|
import '../home/home_logic.dart';
|
||||||
import 'select_contacts/select_contacts_logic.dart';
|
import 'select_contacts/select_contacts_logic.dart';
|
||||||
|
|
||||||
|
class ColleagueRow {
|
||||||
|
ColleagueRow({
|
||||||
|
required this.userID,
|
||||||
|
required this.nickname,
|
||||||
|
required this.faceURL,
|
||||||
|
required this.department,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String userID;
|
||||||
|
final String nickname;
|
||||||
|
final String faceURL;
|
||||||
|
final String department;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
String get subtitle {
|
||||||
|
if (title.isNotEmpty && department.isNotEmpty) {
|
||||||
|
return '$title · $department';
|
||||||
|
}
|
||||||
|
if (title.isNotEmpty) return title;
|
||||||
|
return department;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ContactsLogic extends GetxController
|
class ContactsLogic extends GetxController
|
||||||
implements ViewUserProfileBridge, SelectContactsBridge, ScanBridge {
|
implements ViewUserProfileBridge, SelectContactsBridge, ScanBridge {
|
||||||
final imLogic = Get.find<IMController>();
|
final imLogic = Get.find<IMController>();
|
||||||
final homeLogic = Get.find<HomeLogic>();
|
final homeLogic = Get.find<HomeLogic>();
|
||||||
|
final _directory = DirectoryApi();
|
||||||
|
|
||||||
final friendApplicationList = <UserInfo>[];
|
final friendApplicationList = <UserInfo>[];
|
||||||
final loading = true.obs;
|
final loading = true.obs;
|
||||||
final loadFailed = false.obs;
|
final loadFailed = false.obs;
|
||||||
|
final colleagues = <ColleagueRow>[].obs;
|
||||||
|
|
||||||
int get friendApplicationCount =>
|
int get friendApplicationCount =>
|
||||||
homeLogic.unhandledFriendApplicationCount.value;
|
homeLogic.unhandledFriendApplicationCount.value;
|
||||||
@@ -23,6 +50,21 @@ class ContactsLogic extends GetxController
|
|||||||
int get groupApplicationCount =>
|
int get groupApplicationCount =>
|
||||||
homeLogic.unhandledGroupApplicationCount.value;
|
homeLogic.unhandledGroupApplicationCount.value;
|
||||||
|
|
||||||
|
Map<String, List<ColleagueRow>> get groupedColleagues {
|
||||||
|
final map = <String, List<ColleagueRow>>{};
|
||||||
|
for (final row in colleagues) {
|
||||||
|
final key = row.department.isEmpty ? '未分组' : row.department;
|
||||||
|
map.putIfAbsent(key, () => []).add(row);
|
||||||
|
}
|
||||||
|
final keys = map.keys.toList()
|
||||||
|
..sort((a, b) {
|
||||||
|
if (a == '未分组') return 1;
|
||||||
|
if (b == '未分组') return -1;
|
||||||
|
return a.compareTo(b);
|
||||||
|
});
|
||||||
|
return {for (final k in keys) k: map[k]!};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
PackageBridge.selectContactsBridge = this;
|
PackageBridge.selectContactsBridge = this;
|
||||||
@@ -46,11 +88,7 @@ class ContactsLogic extends GetxController
|
|||||||
OpenIM.iMManager.friendshipManager
|
OpenIM.iMManager.friendshipManager
|
||||||
.getFriendApplicationListAsRecipient(),
|
.getFriendApplicationListAsRecipient(),
|
||||||
OpenIM.iMManager.groupManager.getGroupApplicationListAsRecipient(),
|
OpenIM.iMManager.groupManager.getGroupApplicationListAsRecipient(),
|
||||||
OpenIM.iMManager.friendshipManager.getFriendListPage(
|
_loadColleagues(),
|
||||||
offset: 0,
|
|
||||||
count: 1,
|
|
||||||
filterBlack: true,
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
homeLogic.getUnhandledFriendApplicationCount();
|
homeLogic.getUnhandledFriendApplicationCount();
|
||||||
homeLogic.getUnhandledGroupApplicationCount();
|
homeLogic.getUnhandledGroupApplicationCount();
|
||||||
@@ -62,6 +100,47 @@ class ContactsLogic extends GetxController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadColleagues() async {
|
||||||
|
final friends = <FriendInfo>[];
|
||||||
|
const page = 1000;
|
||||||
|
while (true) {
|
||||||
|
final temp = await OpenIM.iMManager.friendshipManager.getFriendListPage(
|
||||||
|
offset: friends.length,
|
||||||
|
count: page,
|
||||||
|
filterBlack: true,
|
||||||
|
);
|
||||||
|
friends.addAll(temp);
|
||||||
|
if (temp.length < page) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dirById = <String, DirectoryUser>{};
|
||||||
|
try {
|
||||||
|
final items = await _directory.search(keyword: '', limit: 100);
|
||||||
|
dirById = {for (final e in items) e.userID: e};
|
||||||
|
} catch (e, s) {
|
||||||
|
Logger.print('contacts directory e: $e $s');
|
||||||
|
}
|
||||||
|
|
||||||
|
colleagues.assignAll(friends.where((e) => e.userID != null).map((e) {
|
||||||
|
final dir = dirById[e.userID];
|
||||||
|
return ColleagueRow(
|
||||||
|
userID: e.userID!,
|
||||||
|
nickname: (e.remark?.isNotEmpty == true)
|
||||||
|
? e.remark!
|
||||||
|
: (e.nickname?.isNotEmpty == true ? e.nickname! : e.userID!),
|
||||||
|
faceURL: e.faceURL ?? dir?.faceURL ?? '',
|
||||||
|
department: dir?.department ?? '',
|
||||||
|
title: dir?.title ?? '',
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void openColleague(ColleagueRow row) => AppNavigator.startUserProfilePane(
|
||||||
|
userID: row.userID,
|
||||||
|
nickname: row.nickname,
|
||||||
|
faceURL: row.faceURL,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
PackageBridge.selectContactsBridge = null;
|
PackageBridge.selectContactsBridge = null;
|
||||||
|
|||||||
@@ -32,31 +32,116 @@ class ContactsPage extends StatelessWidget {
|
|||||||
if (logic.loadFailed.value) {
|
if (logic.loadFailed.value) {
|
||||||
return CompanyFailView(onRetry: logic.reload);
|
return CompanyFailView(onRetry: logic.reload);
|
||||||
}
|
}
|
||||||
return Column(
|
final grouped = logic.groupedColleagues;
|
||||||
children: [
|
return CustomScrollView(
|
||||||
_entry(
|
slivers: [
|
||||||
icon: Icons.person_add_alt,
|
SliverToBoxAdapter(
|
||||||
label: '新的同事',
|
child: Column(
|
||||||
count: logic.friendApplicationCount,
|
children: [
|
||||||
onTap: logic.newFriend,
|
_entry(
|
||||||
),
|
icon: Icons.person_add_alt,
|
||||||
_entry(
|
label: '新的同事',
|
||||||
icon: Icons.group_outlined,
|
count: logic.friendApplicationCount,
|
||||||
label: '我的群聊',
|
onTap: logic.newFriend,
|
||||||
count: logic.groupApplicationCount,
|
),
|
||||||
onTap: logic.myGroup,
|
_entry(
|
||||||
),
|
icon: Icons.group_outlined,
|
||||||
_entry(
|
label: '我的群聊',
|
||||||
icon: Icons.people_outline,
|
count: logic.groupApplicationCount,
|
||||||
label: '同事',
|
onTap: logic.myGroup,
|
||||||
onTap: logic.myFriend,
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
if (logic.colleagues.isEmpty)
|
||||||
|
const SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: CompanyEmptyView(
|
||||||
|
icon: Icons.person_outline,
|
||||||
|
title: '暂时没有同事',
|
||||||
|
subtitle: '点右上角添加同事',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
for (final entry in grouped.entries) ...[
|
||||||
|
SliverToBoxAdapter(child: _groupHeader(entry.key)),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(_, index) => _colleagueRow(entry.value[index]),
|
||||||
|
childCount: entry.value.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _groupHeader(String name) {
|
||||||
|
return Container(
|
||||||
|
height: 28,
|
||||||
|
color: AppColors.chatBg,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: AppGap.x4),
|
||||||
|
child: Text(
|
||||||
|
name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: AppFont.small, color: AppColors.textSecondary),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _colleagueRow(ColleagueRow row) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => logic.openColleague(row),
|
||||||
|
splashColor: AppColors.chatBg,
|
||||||
|
highlightColor: AppColors.chatBg,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 64,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: AppGap.x4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
AvatarView(
|
||||||
|
url: row.faceURL,
|
||||||
|
text: row.nickname,
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
const SizedBox(width: AppGap.x3),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
row.nickname,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: AppFont.body,
|
||||||
|
color: AppColors.textPrimary),
|
||||||
|
),
|
||||||
|
if (row.subtitle.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
row.subtitle,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: AppFont.sub,
|
||||||
|
color: AppColors.textSecondary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _entry({
|
Widget _entry({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String label,
|
required String label,
|
||||||
|
|||||||
@@ -34,19 +34,111 @@ class FriendRequestsPage extends StatelessWidget {
|
|||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: logic.applicationList.length,
|
itemCount: logic.applicationList.length,
|
||||||
itemBuilder: (_, index) =>
|
itemBuilder: (_, index) =>
|
||||||
_buildItemView(logic.applicationList[index]),
|
_buildItemView(context, logic.applicationList[index]),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildItemView(FriendApplicationInfo info) {
|
Widget _buildItemView(BuildContext context, FriendApplicationInfo info) {
|
||||||
final isISendRequest = info.fromUserID == OpenIM.iMManager.userID;
|
final isISendRequest = info.fromUserID == OpenIM.iMManager.userID;
|
||||||
final name = isISendRequest ? info.toNickname : info.fromNickname;
|
final name = isISendRequest ? info.toNickname : info.fromNickname;
|
||||||
final faceURL = isISendRequest ? info.toFaceURL : info.fromFaceURL;
|
final faceURL = isISendRequest ? info.toFaceURL : info.fromFaceURL;
|
||||||
final waiting = info.isWaitingHandle;
|
final waiting = info.isWaitingHandle;
|
||||||
final rejected = info.isRejected;
|
final rejected = info.isRejected;
|
||||||
final agreed = info.isAgreed;
|
final agreed = info.isAgreed;
|
||||||
|
final narrow = MediaQuery.sizeOf(context).width < 360;
|
||||||
|
final pendingActions = waiting && !isISendRequest;
|
||||||
|
final stacked = narrow && pendingActions;
|
||||||
|
|
||||||
|
Widget status;
|
||||||
|
if (waiting && isISendRequest) {
|
||||||
|
status = const Text('等待对方通过',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppFont.sub, color: AppColors.textSecondary));
|
||||||
|
} else if (pendingActions) {
|
||||||
|
status = SizedBox(
|
||||||
|
width: 132,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () => logic.refuseFriendApplication(info),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
minimumSize: const Size(0, 32),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
side: const BorderSide(
|
||||||
|
color: AppColors.textSecondary, width: 0.5),
|
||||||
|
foregroundColor: AppColors.textPrimary,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(AppRadius.control)),
|
||||||
|
),
|
||||||
|
child: const Text('拒绝',
|
||||||
|
style: TextStyle(fontSize: AppFont.sub)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: AppGap.x2),
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () => logic.acceptFriendApplication(info),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
minimumSize: const Size(0, 32),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
backgroundColor: AppColors.primary,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(AppRadius.control)),
|
||||||
|
),
|
||||||
|
child: const Text('接受',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppFont.sub, color: Colors.white)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (agreed) {
|
||||||
|
status = const Text('已添加',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppFont.sub, color: AppColors.textSecondary));
|
||||||
|
} else if (rejected) {
|
||||||
|
status = const Text('已拒绝',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppFont.sub, color: AppColors.textSecondary));
|
||||||
|
} else {
|
||||||
|
status = const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final identity = Row(
|
||||||
|
children: [
|
||||||
|
AvatarView(url: faceURL, text: name, width: 48, height: 48),
|
||||||
|
const SizedBox(width: AppGap.x3),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
name ?? '',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: AppFont.body, color: AppColors.textPrimary),
|
||||||
|
),
|
||||||
|
if (IMUtils.isNotNullEmptyStr(info.reqMsg))
|
||||||
|
Text(
|
||||||
|
info.reqMsg ?? '',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: AppFont.sub, color: AppColors.textSecondary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
constraints: const BoxConstraints(minHeight: 72),
|
constraints: const BoxConstraints(minHeight: 72),
|
||||||
@@ -57,90 +149,22 @@ class FriendRequestsPage extends StatelessWidget {
|
|||||||
border:
|
border:
|
||||||
Border(bottom: BorderSide(color: AppColors.divider, width: 0.5)),
|
Border(bottom: BorderSide(color: AppColors.divider, width: 0.5)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: stacked
|
||||||
children: [
|
? Column(
|
||||||
AvatarView(url: faceURL, text: name, width: 48, height: 48),
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
const SizedBox(width: AppGap.x3),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
identity,
|
||||||
name ?? '',
|
const SizedBox(height: AppGap.x2),
|
||||||
maxLines: 1,
|
Align(alignment: Alignment.centerRight, child: status),
|
||||||
overflow: TextOverflow.ellipsis,
|
],
|
||||||
style: const TextStyle(
|
)
|
||||||
fontSize: AppFont.body, color: AppColors.textPrimary),
|
: Row(
|
||||||
),
|
children: [
|
||||||
if (IMUtils.isNotNullEmptyStr(info.reqMsg))
|
Expanded(child: identity),
|
||||||
Text(
|
const SizedBox(width: AppGap.x2),
|
||||||
info.reqMsg ?? '',
|
status,
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: AppFont.sub, color: AppColors.textSecondary),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(width: AppGap.x2),
|
|
||||||
if (waiting && isISendRequest)
|
|
||||||
const Text('等待对方通过',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: AppFont.sub, color: AppColors.textSecondary)),
|
|
||||||
if (waiting && !isISendRequest)
|
|
||||||
SizedBox(
|
|
||||||
width: 132,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton(
|
|
||||||
onPressed: () => logic.refuseFriendApplication(info),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
minimumSize: const Size(0, 32),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
side: const BorderSide(
|
|
||||||
color: AppColors.textSecondary, width: 0.5),
|
|
||||||
foregroundColor: AppColors.textPrimary,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(AppRadius.control)),
|
|
||||||
),
|
|
||||||
child: const Text('拒绝',
|
|
||||||
style: TextStyle(fontSize: AppFont.sub)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: AppGap.x2),
|
|
||||||
Expanded(
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed: () => logic.acceptFriendApplication(info),
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
minimumSize: const Size(0, 32),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
backgroundColor: AppColors.primary,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(AppRadius.control)),
|
|
||||||
),
|
|
||||||
child: const Text('接受',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: AppFont.sub, color: Colors.white)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (agreed)
|
|
||||||
const Text('已添加',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: AppFont.sub, color: AppColors.textSecondary)),
|
|
||||||
if (rejected)
|
|
||||||
const Text('已拒绝',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: AppFont.sub, color: AppColors.textSecondary)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user