♻️ Refactored friend module

This commit is contained in:
LittleSheep 2024-07-24 01:17:41 +08:00
parent 39c8597428
commit 8366bda846
18 changed files with 250 additions and 272 deletions

View File

@ -11,11 +11,11 @@ import 'package:solian/platform.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart';
import 'package:solian/providers/content/call.dart';
import 'package:solian/providers/call.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/friend.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/providers/account_status.dart';
import 'package:solian/router.dart';
import 'package:solian/shells/system_shell.dart';
@ -96,7 +96,7 @@ class SolianApp extends StatelessWidget {
void _initializeProviders(BuildContext context) async {
Get.lazyPut(() => AuthProvider());
Get.lazyPut(() => FriendProvider());
Get.lazyPut(() => RelationshipProvider());
Get.lazyPut(() => PostProvider());
Get.lazyPut(() => AttachmentProvider());
Get.lazyPut(() => WebSocketProvider());

View File

@ -1,38 +1,35 @@
import 'package:solian/models/account.dart';
class Friendship {
class Relationship {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
int accountId;
int relatedId;
int? blockedBy;
Account account;
Account related;
int status;
Friendship({
Relationship({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.accountId,
required this.relatedId,
required this.blockedBy,
required this.account,
required this.related,
required this.status,
});
factory Friendship.fromJson(Map<String, dynamic> json) => Friendship(
factory Relationship.fromJson(Map<String, dynamic> json) => Relationship(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'],
accountId: json['account_id'],
relatedId: json['related_id'],
blockedBy: json['blocked_by'],
account: Account.fromJson(json['account']),
related: Account.fromJson(json['related']),
status: json['status'],
@ -45,7 +42,6 @@ class Friendship {
'deleted_at': deletedAt,
'account_id': accountId,
'related_id': relatedId,
'blocked_by': blockedBy,
'account': account.toJson(),
'related': related.toJson(),
'status': status,

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/widgets/account/friend_select.dart';
import 'package:solian/widgets/account/relative_select.dart';
import 'package:uuid/uuid.dart';
class ChannelProvider extends GetxController {
@ -123,7 +123,7 @@ class ChannelProvider extends GetxController {
final related = await showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => FriendSelect(
builder: (context) => RelativeSelector(
title: 'channelOrganizeDirectHint'.tr,
),
);

View File

@ -1,43 +0,0 @@
import 'package:get/get.dart';
import 'package:solian/models/friendship.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
class FriendProvider extends GetConnect {
@override
void onInit() {
final AuthProvider auth = Get.find();
httpClient.baseUrl = ServiceFinder.buildUrl('auth', null);
httpClient.addAuthenticator(auth.requestAuthenticator);
}
Future<Response> listFriendship() => get('/users/me/friends');
Future<Response> listFriendshipWithStatus(int status) =>
get('/users/me/friends?status=$status');
Future<Response> createFriendship(String username) async {
final resp = await post('/users/me/friends?related=$username', {});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> updateFriendship(Friendship relationship, int status) async {
final AuthProvider auth = Get.find();
final prof = await auth.getProfile();
final otherside = relationship.getOtherside(prof.body['id']);
final resp = await put('/users/me/friends/${otherside.id}', {
'status': status,
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
}

View File

@ -0,0 +1,57 @@
import 'package:get/get.dart';
import 'package:solian/models/relations.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
class RelationshipProvider extends GetConnect {
@override
void onInit() {
final AuthProvider auth = Get.find();
httpClient.baseUrl = ServiceFinder.buildUrl('auth', null);
httpClient.addAuthenticator(auth.requestAuthenticator);
}
Future<Response> listRelation() => get('/users/me/relations');
Future<Response> listRelationWithStatus(int status) =>
get('/users/me/relations?status=$status');
Future<Response> makeFriend(String username) async {
final resp = await post('/users/me/relations?related=$username', {});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> handleRelation(
Relationship relationship, bool doAccept) async {
final AuthProvider auth = Get.find();
final client = auth.configureClient('auth');
final resp = await client.post(
'/users/me/relations/${relationship.relatedId}/${doAccept ? 'accept' : 'decline'}',
{},
);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
Future<Response> editRelation(Relationship relationship, int status) async {
final AuthProvider auth = Get.find();
final client = auth.configureClient('auth');
final resp =
await client.patch('/users/me/relations/${relationship.relatedId}', {
'status': status,
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
}
return resp;
}
}

View File

@ -191,10 +191,7 @@ abstract class AppRouter {
GoRoute(
path: '/account/friend',
name: 'accountFriend',
builder: (context, state) => TitleShell(
state: state,
child: const FriendScreen(),
),
builder: (context, state) => const FriendScreen(),
),
GoRoute(
path: '/account/personalize',

View File

@ -2,10 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/friendship.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/friend.dart';
import 'package:solian/widgets/account/friend_list.dart';
import 'package:solian/models/relations.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/widgets/account/relative_list.dart';
class FriendScreen extends StatefulWidget {
const FriendScreen({super.key});
@ -14,71 +13,38 @@ class FriendScreen extends StatefulWidget {
State<FriendScreen> createState() => _FriendScreenState();
}
class _FriendScreenState extends State<FriendScreen> {
class _FriendScreenState extends State<FriendScreen>
with SingleTickerProviderStateMixin {
late final TabController _tabController;
bool _isBusy = false;
int? _accountId;
List<Friendship> _friendships = List.empty();
List<Relationship> _relations = List.empty();
List<Friendship> filterWithStatus(int status) {
return _friendships.where((x) => x.status == status).toList();
List<Relationship> filterByStatus(int status) {
return _relations.where((x) => x.status == status).toList();
}
Future<void> getFriendship() async {
Future<void> loadRelations() async {
setState(() => _isBusy = true);
final FriendProvider provider = Get.find();
final resp = await provider.listFriendship();
final RelationshipProvider provider = Get.find();
final resp = await provider.listRelation();
setState(() {
_friendships = resp.body
.map((e) => Friendship.fromJson(e))
_relations = resp.body
.map((e) => Relationship.fromJson(e))
.toList()
.cast<Friendship>();
.cast<Relationship>();
_isBusy = false;
});
}
void showScopedListPopup(String title, int status) {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
context: context,
builder: (context) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.85,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded(
child: CustomScrollView(
slivers: [
SliverFriendList(
accountId: _accountId!,
items: filterWithStatus(status),
onUpdate: () {
getFriendship();
},
),
],
),
),
],
),
);
},
);
}
void promptAddFriend() async {
final FriendProvider provider = Get.find();
final RelationshipProvider provider = Get.find();
final controller = TextEditingController();
final input = await showDialog(
final input = await showDialog<String?>(
context: context,
builder: (context) {
return AlertDialog(
@ -125,7 +91,7 @@ class _FriendScreenState extends State<FriendScreen> {
try {
setState(() => _isBusy = true);
await provider.createFriendship(input);
await provider.makeFriend(input);
} catch (e) {
context.showErrorDialog(e);
} finally {
@ -135,12 +101,14 @@ class _FriendScreenState extends State<FriendScreen> {
@override
void initState() {
Get.find<AuthProvider>().getProfile().then((value) {
_accountId = value.body['id'];
});
super.initState();
_tabController = TabController(length: 3, vsync: this);
Future.delayed(Duration.zero, () => getFriendship());
loadRelations().then((_) {
if (filterByStatus(0).isEmpty) {
_tabController.animateTo(1);
}
});
}
@override
@ -148,64 +116,71 @@ class _FriendScreenState extends State<FriendScreen> {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Scaffold(
appBar: AppBar(
centerTitle: false,
title: Text('accountFriend'.tr),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(icon: Icon(Icons.call_received)),
Tab(icon: Icon(Icons.people)),
Tab(icon: Icon(Icons.call_made)),
],
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => promptAddFriend(),
),
body: RefreshIndicator(
onRefresh: () => getFriendship(),
child: CustomScrollView(
slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
SliverToBoxAdapter(
child: ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
leading: const Icon(Icons.person_add),
trailing: const Icon(Icons.chevron_right),
title: Text(
'${'accountFriendPending'.tr} (${filterWithStatus(0).length})',
body: TabBarView(
controller: _tabController,
children: [
RefreshIndicator(
onRefresh: () => loadRelations(),
child: CustomScrollView(
slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
SilverRelativeList(
items: filterByStatus(0),
onUpdate: () => loadRelations(),
),
onTap: () =>
showScopedListPopup('accountFriendPending'.tr, 0),
),
],
),
SliverToBoxAdapter(
child: ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
leading: const Icon(Icons.block),
trailing: const Icon(Icons.chevron_right),
title: Text(
'${'accountFriendBlocked'.tr} (${filterWithStatus(2).length})',
),
RefreshIndicator(
onRefresh: () => loadRelations(),
child: CustomScrollView(
slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
SilverRelativeList(
items: filterByStatus(1),
onUpdate: () => loadRelations(),
),
onTap: () =>
showScopedListPopup('accountFriendBlocked'.tr, 2),
),
],
),
if (_accountId != null)
SliverFriendList(
accountId: _accountId!,
items: filterWithStatus(1),
onUpdate: () {
getFriendship();
},
),
const SliverToBoxAdapter(
child: Divider(thickness: 0.3, height: 0.3),
),
RefreshIndicator(
onRefresh: () => loadRelations(),
child: CustomScrollView(
slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
SilverRelativeList(
items: filterByStatus(3),
onUpdate: () => loadRelations(),
),
],
),
SliverToBoxAdapter(
child: Text(
'accountFriendListHint'.tr,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
).paddingOnly(top: 16, bottom: 32),
),
],
),
),
],
),
),
);

View File

@ -3,7 +3,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/providers/content/call.dart';
import 'package:solian/providers/call.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/chat/call/call_controls.dart';

View File

@ -11,7 +11,7 @@ import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/models/packet.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/call.dart';
import 'package:solian/providers/call.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/router.dart';

View File

@ -1,87 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/friendship.dart';
import 'package:solian/providers/friend.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart';
class SliverFriendList extends StatelessWidget {
final int accountId;
final List<Friendship> items;
final Function onUpdate;
const SliverFriendList({
super.key,
required this.accountId,
required this.items,
required this.onUpdate,
});
DismissDirection getDismissDirection(Friendship relation) {
if (relation.status == 2) return DismissDirection.endToStart;
if (relation.status == 1) return DismissDirection.startToEnd;
if (relation.status == 0 && relation.relatedId != accountId) {
return DismissDirection.startToEnd;
}
return DismissDirection.horizontal;
}
Widget buildItem(context, index) {
final element = items[index];
final otherside = element.getOtherside(accountId);
final randomId = DateTime.now().microsecondsSinceEpoch >> 10;
return Dismissible(
key: Key(randomId.toString()),
background: Container(
color: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 20),
alignment: Alignment.centerLeft,
child: const Icon(Icons.close, color: Colors.white),
),
secondaryBackground: Container(
color: Colors.green,
padding: const EdgeInsets.symmetric(horizontal: 20),
alignment: Alignment.centerRight,
child: const Icon(Icons.check, color: Colors.white),
),
direction: getDismissDirection(element),
child: ListTile(
title: Text(otherside.nick),
subtitle: Text(otherside.name),
leading: GestureDetector(
child: AccountAvatar(content: otherside.avatar),
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) => AccountProfilePopup(
account: otherside,
),
);
},
),
),
onDismissed: (direction) {
final FriendProvider provider = Get.find();
if (direction == DismissDirection.startToEnd) {
provider.updateFriendship(element, 2).then((_) => onUpdate());
}
if (direction == DismissDirection.endToStart) {
provider.updateFriendship(element, 1).then((_) => onUpdate());
}
},
);
}
@override
Widget build(BuildContext context) {
return SliverList.builder(
itemCount: items.length,
itemBuilder: (context, idx) => buildItem(context, idx),
);
}
}

View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/relations.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart';
class SilverRelativeList extends StatelessWidget {
final List<Relationship> items;
final Function onUpdate;
final bool isHandleable;
const SilverRelativeList({
super.key,
required this.items,
required this.onUpdate,
this.isHandleable = true,
});
Widget buildItem(context, index) {
final element = items[index];
return ListTile(
title: Text(element.related.nick),
subtitle: Text(element.related.name),
leading: GestureDetector(
child: AccountAvatar(content: element.related.avatar),
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme
.of(context)
.colorScheme
.surface,
context: context,
builder: (context) =>
AccountProfilePopup(
account: element.related,
),
);
},
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if(element.status != 1 && element.status != 3)
IconButton(
icon: const Icon(Icons.check),
onPressed: () {
final RelationshipProvider provider = Get.find();
if (element.status == 0) {
provider.handleRelation(element, true).then((_) => onUpdate());
} else {
provider.editRelation(element, 1).then((_) => onUpdate());
}
},
),
if(element.status != 2 && element.status != 3)
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
final RelationshipProvider provider = Get.find();
if (element.status == 0) {
provider.handleRelation(element, false).then((_) => onUpdate());
} else {
provider.editRelation(element, 2).then((_) => onUpdate());
}
},
),
],
),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
);
}
@override
Widget build(BuildContext context) {
return SliverList.builder(
itemCount: items.length,
itemBuilder: (context, idx) => buildItem(context, idx),
);
}
}

View File

@ -1,39 +1,39 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/friendship.dart';
import 'package:solian/models/relations.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/friend.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/widgets/account/account_avatar.dart';
class FriendSelect extends StatefulWidget {
class RelativeSelector extends StatefulWidget {
final String title;
final Widget? Function(Account item)? trailingBuilder;
const FriendSelect({super.key, required this.title, this.trailingBuilder});
const RelativeSelector({super.key, required this.title, this.trailingBuilder});
@override
State<FriendSelect> createState() => _FriendSelectState();
State<RelativeSelector> createState() => _RelativeSelectorState();
}
class _FriendSelectState extends State<FriendSelect> {
class _RelativeSelectorState extends State<RelativeSelector> {
int _accountId = 0;
final List<Friendship> _friends = List.empty(growable: true);
final List<Relationship> _friends = List.empty(growable: true);
getFriends() async {
final AuthProvider auth = Get.find();
final prof = await auth.getProfile();
_accountId = prof.body['id'];
final FriendProvider provider = Get.find();
final resp = await provider.listFriendshipWithStatus(1);
final RelationshipProvider provider = Get.find();
final resp = await provider.listRelationWithStatus(1);
setState(() {
_friends.addAll(resp.body
.map((e) => Friendship.fromJson(e))
.map((e) => Relationship.fromJson(e))
.toList()
.cast<Friendship>());
.cast<Relationship>());
});
}

View File

@ -7,7 +7,7 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/account/friend_select.dart';
import 'package:solian/widgets/account/relative_select.dart';
class ChannelMemberListPopup extends StatefulWidget {
final Channel channel;
@ -62,7 +62,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
final input = await showModalBottomSheet(
context: context,
builder: (context) {
return FriendSelect(title: 'channelMembersAdd'.tr);
return RelativeSelector(title: 'channelMembersAdd'.tr);
},
);
if (input == null) return;

View File

@ -6,7 +6,7 @@ import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:get/get.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:solian/exts.dart';
import 'package:solian/providers/content/call.dart';
import 'package:solian/providers/call.dart';
class ControlsWidget extends StatefulWidget {
final Room room;

View File

@ -6,7 +6,7 @@ import 'package:solian/exts.dart';
import 'package:solian/models/call.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/call.dart';
import 'package:solian/providers/call.dart';
class ChatCallPrejoinPopup extends StatefulWidget {
final Call ongoingCall;

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/providers/content/call.dart';
import 'package:solian/providers/call.dart';
class ChatCallCurrentIndicator extends StatelessWidget {
const ChatCallCurrentIndicator({super.key});

View File

@ -7,7 +7,7 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/account/friend_select.dart';
import 'package:solian/widgets/account/relative_select.dart';
class RealmMemberListPopup extends StatefulWidget {
final Realm realm;
@ -59,7 +59,7 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
final input = await showModalBottomSheet(
context: context,
builder: (context) {
return FriendSelect(title: 'channelMembersAdd'.tr);
return RelativeSelector(title: 'channelMembersAdd'.tr);
},
);
if (input == null) return;