From 1f5585fbfdb5d140fdd7ab2aa8e7eec616e3cbfb Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Wed, 16 Nov 2022 02:18:27 +1030 Subject: [PATCH] Change list layouts to fix pull to refresh --- Backend/Api/Messages/Conversations.go | 14 ++ Backend/Database/ConversationDetails.go | 18 +- mobile/lib/components/custom_title_bar.dart | 3 - .../repositories/friends_repository.dart | 1 + mobile/lib/main.dart | 8 +- .../lib/services/conversations_service.dart | 12 +- mobile/lib/views/main/conversation/list.dart | 157 ++++++++++-------- mobile/lib/views/main/friend/list.dart | 138 +++++++-------- mobile/lib/views/main/friend/list_item.dart | 4 +- mobile/lib/views/main/home.dart | 67 ++++---- 10 files changed, 224 insertions(+), 198 deletions(-) diff --git a/Backend/Api/Messages/Conversations.go b/Backend/Api/Messages/Conversations.go index 74dbf8a..1f55dfa 100644 --- a/Backend/Api/Messages/Conversations.go +++ b/Backend/Api/Messages/Conversations.go @@ -7,6 +7,7 @@ import ( "net/url" "strconv" "strings" + "time" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" @@ -58,6 +59,8 @@ func ConversationDetailsList(w http.ResponseWriter, r *http.Request) { detail Database.ConversationDetail query url.Values conversationIds []string + updatedAtString []string + updatedAt time.Time messageExpiryRaw driver.Value returnJSON []byte i int @@ -66,6 +69,7 @@ func ConversationDetailsList(w http.ResponseWriter, r *http.Request) { ) query = r.URL.Query() + conversationIds, ok = query["conversation_detail_ids"] if !ok { http.Error(w, "Invalid Data", http.StatusBadGateway) @@ -74,8 +78,18 @@ func ConversationDetailsList(w http.ResponseWriter, r *http.Request) { conversationIds = strings.Split(conversationIds[0], ",") + updatedAtString, ok = query["updated_at"] + if ok { + updatedAt, err = time.Parse(time.RFC3339, updatedAtString[0]) + if err != nil { + http.Error(w, "Invalid Data", http.StatusBadGateway) + return + } + } + conversationDetails, err = Database.GetConversationDetailsByIds( conversationIds, + updatedAt, ) if err != nil { http.Error(w, "Error", http.StatusInternalServerError) diff --git a/Backend/Database/ConversationDetails.go b/Backend/Database/ConversationDetails.go index ab6d664..70cd6d6 100644 --- a/Backend/Database/ConversationDetails.go +++ b/Backend/Database/ConversationDetails.go @@ -1,6 +1,8 @@ package Database import ( + "time" + "github.com/gofrs/uuid" "gorm.io/gorm" @@ -20,6 +22,8 @@ type ConversationDetail struct { AdminAddMembers string ` json:"admin_add_members"` // Stored encrypted AdminEditInfo string ` json:"admin_edit_info"` // Stored encrypted AdminSendMessages string ` json:"admin_send_messages"` // Stored encrypted + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // GetConversationDetailByID gets by id @@ -38,15 +42,21 @@ func GetConversationDetailByID(id string) (ConversationDetail, error) { } // GetConversationDetailsByIds gets by multiple ids -func GetConversationDetailsByIds(id []string) ([]ConversationDetail, error) { +func GetConversationDetailsByIds(id []string, updatedAt time.Time) ([]ConversationDetail, error) { var ( + query *gorm.DB conversationDetail []ConversationDetail err error ) - err = DB.Preload(clause.Associations). - Where("id IN ?", id). - Find(&conversationDetail). + query = DB.Preload(clause.Associations). + Where("id IN ?", id) + + if !updatedAt.IsZero() { + query = query.Where("updated_at > ?", updatedAt) + } + + err = query.Find(&conversationDetail). Error return conversationDetail, err diff --git a/mobile/lib/components/custom_title_bar.dart b/mobile/lib/components/custom_title_bar.dart index 4e0cb52..b8fbd6d 100644 --- a/mobile/lib/components/custom_title_bar.dart +++ b/mobile/lib/components/custom_title_bar.dart @@ -37,9 +37,6 @@ class CustomTitleBar extends StatelessWidget with PreferredSizeWidget { foregroundColor: forgroundColor != null ? forgroundColor! : Theme.of(context).appBarTheme.foregroundColor, - systemOverlayStyle: const SystemUiOverlayStyle( - statusBarColor: Colors.white, - ), flexibleSpace: SafeArea( child: Container( padding: const EdgeInsets.only(right: 16), diff --git a/mobile/lib/database/repositories/friends_repository.dart b/mobile/lib/database/repositories/friends_repository.dart index c56dd28..64bf442 100644 --- a/mobile/lib/database/repositories/friends_repository.dart +++ b/mobile/lib/database/repositories/friends_repository.dart @@ -45,6 +45,7 @@ class FriendsRepository { final List> maps = await db.query( 'friends', where: where, + orderBy: 'accepted_at IS NOT NULL', ); return List.generate(maps.length, (i) { diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 4e1edd1..8da2220 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -134,6 +134,7 @@ class MyApp extends StatelessWidget { iconTheme: const IconThemeData( color: Colors.black, ), + toolbarTextStyle: const TextStyle( color: Colors.black, ), @@ -203,15 +204,10 @@ class MyApp extends StatelessWidget { iconTheme: IconThemeData( color: Colors.grey.shade400 ), + toolbarTextStyle: TextStyle( color: Colors.grey.shade400 ), - systemOverlayStyle: const SystemUiOverlayStyle( - statusBarColor: Colors.black, - - statusBarIconBrightness: Brightness.dark, - statusBarBrightness: Brightness.dark, - ) ), iconTheme: const IconThemeData(color: Colors.white), diff --git a/mobile/lib/services/conversations_service.dart b/mobile/lib/services/conversations_service.dart index dcc42b1..d5cf1e0 100644 --- a/mobile/lib/services/conversations_service.dart +++ b/mobile/lib/services/conversations_service.dart @@ -179,10 +179,11 @@ class ConversationsService { ); if (maps.length != 1) { - throw ArgumentError('Invalid user id'); + conversation.name = 'TODO: Fix this'; + } else { + conversation.name = maps[0]['username']; } - conversation.name = maps[0]['username']; } else { conversation.name = AesHelper.aesDecrypt( base64.decode(conversation.symmetricKey), @@ -220,7 +221,7 @@ class ConversationsService { } } - static Future updateConversations() async { + static Future updateConversations({DateTime? updatedAt}) async { _BaseConversationsResult baseConvs = await _getBaseConversations(); if (baseConvs.detailIds.isEmpty) { @@ -228,6 +229,11 @@ class ConversationsService { } Map params = {}; + + if (updatedAt != null) { + params['updated_at'] = updatedAt.toIso8601String(); + } + params['conversation_detail_ids'] = baseConvs.detailIds.join(','); var uri = await MyProfile.getServerUrl('api/v1/auth/conversation_details'); uri = uri.replace(queryParameters: params); diff --git a/mobile/lib/views/main/conversation/list.dart b/mobile/lib/views/main/conversation/list.dart index 59ce4b1..1836724 100644 --- a/mobile/lib/views/main/conversation/list.dart +++ b/mobile/lib/views/main/conversation/list.dart @@ -28,6 +28,8 @@ class ConversationList extends StatefulWidget { } class _ConversationListState extends State { + final GlobalKey _refreshIndicatorKey = GlobalKey(); + List conversations = []; List friends = []; @@ -45,73 +47,78 @@ class _ConversationListState extends State { showBack: false, backgroundColor: Colors.transparent, ), - body: Padding( - padding: const EdgeInsets.only(top: 16,left: 16,right: 16), - child: SingleChildScrollView( - child: Column( - children: [ - TextField( - decoration: const InputDecoration( - hintText: 'Search...', - prefixIcon: Icon( - Icons.search, - size: 20 - ), - ), - onChanged: (value) => filterSearchResults(value.toLowerCase()) - ), - list(), + body: Padding( + padding: const EdgeInsets.only(top: 16,left: 16,right: 16, bottom: 65), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(top: 50), + child: RefreshIndicator( + key: _refreshIndicatorKey, + onRefresh: _refresh, + child: list(), + ), + ), + TextField( + decoration: const InputDecoration( + hintText: 'Search...', + prefixIcon: Icon( + Icons.search, + size: 20 + ), + ), + onChanged: (value) => filterSearchResults(value.toLowerCase()) + ), ], ), ), - ), - floatingActionButton: Padding( - padding: const EdgeInsets.only(right: 10, bottom: 10), - child: FloatingActionButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ConversationEditDetails( - saveCallback: (String conversationName, File? file) { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ConversationAddFriendsList( - friends: friends, - saveCallback: (List friendsSelected) async { - Conversation conversation = await ConversationsRepository.createConversation( - conversationName, - friendsSelected, - false, - ); - - ConversationsService.uploadConversation(conversation) - .catchError((dynamic) { - showMessage('Failed to create conversation', context); - }); - - if (!mounted) { - return; - } - - Navigator.of(context).popUntil((route) => route.isFirst); - Navigator.push(context, MaterialPageRoute(builder: (context) { - return ConversationDetail( - conversation: conversation, + floatingActionButton: Padding( + padding: const EdgeInsets.only(right: 10, bottom: 60), + child: FloatingActionButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ConversationEditDetails( + saveCallback: (String conversationName, File? file) { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ConversationAddFriendsList( + friends: friends, + saveCallback: (List friendsSelected) async { + Conversation conversation = await ConversationsRepository.createConversation( + conversationName, + friendsSelected, + false, ); - })); - }, - )) - ); - }, - )), - ).then(onGoBack); - }, - backgroundColor: Theme.of(context).colorScheme.primary, - child: Icon( - Icons.add, - size: 30, - color: Theme.of(context).colorScheme.onPrimary, + + ConversationsService.uploadConversation(conversation) + .catchError((dynamic) { + showMessage('Failed to create conversation', context); + }); + + if (!mounted) { + return; + } + + Navigator.of(context).popUntil((route) => route.isFirst); + Navigator.push(context, MaterialPageRoute(builder: (context) { + return ConversationDetail( + conversation: conversation, + ); + })); + }, + )) + ); + }, + )), + ).then(onGoBack); + }, + backgroundColor: Theme.of(context).colorScheme.primary, + child: Icon( + Icons.add, + size: 30, + color: Theme.of(context).colorScheme.onPrimary, + ), ), ), - ), ); } @@ -150,20 +157,28 @@ class _ConversationListState extends State { Widget list() { if (conversations.isEmpty) { return const Center( - child: Text('No Conversations'), + child: Text('No Conversations'), ); } return ListView.builder( - itemCount: conversations.length, - shrinkWrap: true, - padding: const EdgeInsets.only(top: 16), - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, i) { - return ConversationListItem( - conversation: conversations[i], - ); - }, + itemCount: conversations.length, + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (context, i) { + return ConversationListItem( + conversation: conversations[i], + ); + }, + ); + } + + Future _refresh() async { + Future.delayed( + const Duration(seconds: 1), + () { + print('Doing thing'); + } ); } diff --git a/mobile/lib/views/main/friend/list.dart b/mobile/lib/views/main/friend/list.dart index c7c6219..93d21a4 100644 --- a/mobile/lib/views/main/friend/list.dart +++ b/mobile/lib/views/main/friend/list.dart @@ -11,13 +11,11 @@ import '/views/main/friend/request_list_item.dart'; class FriendList extends StatefulWidget { final List friends; - final List friendRequests; final Function callback; const FriendList({ Key? key, required this.friends, - required this.friendRequests, required this.callback, }) : super(key: key); @@ -26,11 +24,10 @@ class FriendList extends StatefulWidget { } class _FriendListState extends State { - List friends = []; - List friendRequests = []; + final GlobalKey _refreshIndicatorKey = GlobalKey(); + List friends = []; List friendsDuplicate = []; - List friendRequestsDuplicate = []; @override Widget build(BuildContext context) { @@ -46,31 +43,33 @@ class _FriendListState extends State { showBack: false, backgroundColor: Colors.transparent, ), - body: Padding( - padding: const EdgeInsets.only(top: 16,left: 16,right: 16), - child: SingleChildScrollView( - child: Column( - children: [ - TextField( - decoration: const InputDecoration( - hintText: 'Search...', - prefixIcon: Icon( - Icons.search, - size: 20 - ), - ), - onChanged: (value) => filterSearchResults(value.toLowerCase()) - ), - headingOrNull('Friend Requests'), - friendRequestList(), - headingOrNull('Friends'), - friendList(), + body: Padding( + padding: const EdgeInsets.only(top: 16,left: 16,right: 16, bottom: 65), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only(top: 50), + child: RefreshIndicator( + key: _refreshIndicatorKey, + onRefresh: _refresh, + child: list(), + ) + ), + TextField( + decoration: const InputDecoration( + hintText: 'Search...', + prefixIcon: Icon( + Icons.search, + size: 20 + ), + ), + onChanged: (value) => filterSearchResults(value.toLowerCase()) + ), ], ), ), - ), floatingActionButton: Padding( - padding: const EdgeInsets.only(right: 10, bottom: 10), + padding: const EdgeInsets.only(right: 10, bottom: 60), child: ExpandableFab( icon: Icon( Icons.add, @@ -96,20 +95,17 @@ class _FriendListState extends State { icon: const Icon(Icons.search, size: 25), ), ], - ) + ) ) ); } void filterSearchResults(String query) { List dummyFriendsList = []; - List dummyFriendRequestsList = []; dummyFriendsList.addAll(friends); - dummyFriendRequestsList.addAll(friendRequests); if (query.isNotEmpty) { List dummyFriendData = []; - List dummyFriendRequestData = []; for (Friend item in dummyFriendsList) { if(item.username.toLowerCase().contains(query)) { @@ -117,27 +113,17 @@ class _FriendListState extends State { } } - for (Friend item in dummyFriendRequestsList) { - if(item.username.toLowerCase().contains(query)) { - dummyFriendRequestData.add(item); - } - } - - setState(() { friends.clear(); friends.addAll(dummyFriendData); - friendRequests.clear(); - friendRequests.addAll(dummyFriendRequestData); }); + return; } setState(() { friends.clear(); friends.addAll(widget.friends); - friendRequests.clear(); - friendRequests.addAll(widget.friendRequests); }); } @@ -145,23 +131,17 @@ class _FriendListState extends State { void initState() { super.initState(); friends.addAll(widget.friends); - friendRequests.addAll(widget.friendRequests); } Future initFriends() async { - friends = await FriendsRepository.getFriends(accepted: true); - friendRequests = await FriendsRepository.getFriends(accepted: false); + friends = await FriendsRepository.getFriends(); setState(() {}); widget.callback(); } - Widget headingOrNull(String heading) { - if (friends.isEmpty || friendRequests.isEmpty) { - return const SizedBox.shrink(); - } - + Widget _heading(String heading) { return Padding( - padding: const EdgeInsets.only(top: 16), + padding: const EdgeInsets.only(top: 5, bottom: 10), child: Align( alignment: Alignment.centerLeft, child: Text( @@ -175,42 +155,50 @@ class _FriendListState extends State { ); } - Widget friendRequestList() { - if (friendRequests.isEmpty) { - return const SizedBox.shrink(); - } - - return ListView.builder( - itemCount: friendRequests.length, - shrinkWrap: true, - padding: const EdgeInsets.only(top: 16), - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, i) { - return FriendRequestListItem( - friend: friendRequests[i], - callback: initFriends, - ); - }, - ); - } - - Widget friendList() { + Widget list() { if (friends.isEmpty) { return const Center( child: Text('No Friends'), ); } - return ListView.builder( + return ListView.separated( itemCount: friends.length, - shrinkWrap: true, - padding: const EdgeInsets.only(top: 16), - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, i) { + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only(top: 10), + separatorBuilder: (context, i) { + if (friends[i].acceptedAt == null) { + return FriendRequestListItem( + friend: friends[i], + callback: initFriends, + ); + } + return FriendListItem( friend: friends[i], ); }, + itemBuilder: (context, i) { + if (i == 0 && friends[i].acceptedAt == null) { + return _heading('Friend Requests'); + } + + if ((i == 0 || friends[i - 1].acceptedAt == null) && friends[i].acceptedAt != null) { + return _heading('Friends'); + } + + return const SizedBox.shrink(); + }, + ); + } + + Future _refresh() async { + Future.delayed( + const Duration(seconds: 1), + () { + print('Doing thing'); + } ); } } diff --git a/mobile/lib/views/main/friend/list_item.dart b/mobile/lib/views/main/friend/list_item.dart index d140ac7..0999514 100644 --- a/mobile/lib/views/main/friend/list_item.dart +++ b/mobile/lib/views/main/friend/list_item.dart @@ -26,7 +26,9 @@ class _FriendListItemState extends State { Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () { findOrCreateConversation(context); }, + onTap: () { + findOrCreateConversation(context); + }, child: Container( padding: const EdgeInsets.only(left: 16,right: 16,top: 0,bottom: 20), child: Row( diff --git a/mobile/lib/views/main/home.dart b/mobile/lib/views/main/home.dart index adc4935..ad092f5 100644 --- a/mobile/lib/views/main/home.dart +++ b/mobile/lib/views/main/home.dart @@ -35,7 +35,7 @@ class _HomeState extends State { int _selectedIndex = 0; List _widgetOptions = [ const ConversationList(conversations: [], friends: []), - FriendList(friends: const [], friendRequests: const [], callback: () {}), + FriendList(friends: const [], callback: () {}), Profile( profile: MyProfile( id: '', @@ -50,34 +50,35 @@ class _HomeState extends State { return WillPopScope( onWillPop: () async => false, child: isLoading ? loading() : Scaffold( - body: _widgetOptions.elementAt(_selectedIndex), - bottomNavigationBar: isLoading ? - const SizedBox.shrink() : - BottomNavigationBar( - currentIndex: _selectedIndex, - onTap: _onItemTapped, - selectedItemColor: Theme.of(context).primaryColor, - unselectedItemColor: Theme.of(context).hintColor, - selectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600), - unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600), - backgroundColor: Colors.transparent, - elevation: 0, - type: BottomNavigationBarType.fixed, - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.message), - label: 'Chats', - ), - BottomNavigationBarItem( - icon: Icon(Icons.group_work), - label: 'Friends', - ), - BottomNavigationBarItem( - icon: Icon(Icons.account_box), - label: 'Profile', - ), - ], - ), + extendBody: true, + body: _widgetOptions.elementAt(_selectedIndex), + bottomNavigationBar: isLoading ? + const SizedBox.shrink() : + BottomNavigationBar( + currentIndex: _selectedIndex, + onTap: _onItemTapped, + selectedItemColor: Theme.of(context).primaryColor, + unselectedItemColor: Theme.of(context).hintColor, + selectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600), + unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w600), + backgroundColor: Colors.transparent, + elevation: 0, + type: BottomNavigationBarType.fixed, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.message), + label: 'Chats', + ), + BottomNavigationBarItem( + icon: Icon(Icons.group_work), + label: 'Friends', + ), + BottomNavigationBarItem( + icon: Icon(Icons.account_box), + label: 'Profile', + ), + ], + ), ), ); } @@ -160,8 +161,7 @@ class _HomeState extends State { await MessagesService.updateMessageThreads(); conversations = await ConversationsRepository.getConversations(); - friends = await FriendsRepository.getFriends(accepted: true); - friendRequests = await FriendsRepository.getFriends(accepted: false); + friends = await FriendsRepository.getFriends(); profile = await MyProfile.getProfile(); setState(() { @@ -172,7 +172,6 @@ class _HomeState extends State { ), FriendList( friends: friends, - friendRequests: friendRequests, callback: reinitDatabaseRecords, ), Profile(profile: profile), @@ -184,8 +183,7 @@ class _HomeState extends State { Future reinitDatabaseRecords() async { conversations = await ConversationsRepository.getConversations(); - friends = await FriendsRepository.getFriends(accepted: true); - friendRequests = await FriendsRepository.getFriends(accepted: false); + friends = await FriendsRepository.getFriends(); profile = await MyProfile.getProfile(); setState(() { @@ -196,7 +194,6 @@ class _HomeState extends State { ), FriendList( friends: friends, - friendRequests: friendRequests, callback: reinitDatabaseRecords, ), Profile(profile: profile),