Browse Source

Change list layouts to fix pull to refresh

develop
Tovi Jaeschke-Rogers 2 years ago
parent
commit
1f5585fbfd
10 changed files with 224 additions and 198 deletions
  1. +14
    -0
      Backend/Api/Messages/Conversations.go
  2. +14
    -4
      Backend/Database/ConversationDetails.go
  3. +0
    -3
      mobile/lib/components/custom_title_bar.dart
  4. +1
    -0
      mobile/lib/database/repositories/friends_repository.dart
  5. +2
    -6
      mobile/lib/main.dart
  6. +9
    -3
      mobile/lib/services/conversations_service.dart
  7. +86
    -71
      mobile/lib/views/main/conversation/list.dart
  8. +63
    -75
      mobile/lib/views/main/friend/list.dart
  9. +3
    -1
      mobile/lib/views/main/friend/list_item.dart
  10. +32
    -35
      mobile/lib/views/main/home.dart

+ 14
- 0
Backend/Api/Messages/Conversations.go View File

@ -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)


+ 14
- 4
Backend/Database/ConversationDetails.go View File

@ -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


+ 0
- 3
mobile/lib/components/custom_title_bar.dart View File

@ -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),


+ 1
- 0
mobile/lib/database/repositories/friends_repository.dart View File

@ -45,6 +45,7 @@ class FriendsRepository {
final List<Map<String, dynamic>> maps = await db.query(
'friends',
where: where,
orderBy: 'accepted_at IS NOT NULL',
);
return List.generate(maps.length, (i) {


+ 2
- 6
mobile/lib/main.dart View File

@ -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),


+ 9
- 3
mobile/lib/services/conversations_service.dart View File

@ -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<void> updateConversations() async {
static Future<void> updateConversations({DateTime? updatedAt}) async {
_BaseConversationsResult baseConvs = await _getBaseConversations();
if (baseConvs.detailIds.isEmpty) {
@ -228,6 +229,11 @@ class ConversationsService {
}
Map<String, String> 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);


+ 86
- 71
mobile/lib/views/main/conversation/list.dart View File

@ -28,6 +28,8 @@ class ConversationList extends StatefulWidget {
}
class _ConversationListState extends State<ConversationList> {
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
List<Conversation> conversations = [];
List<Friend> friends = [];
@ -45,73 +47,78 @@ class _ConversationListState extends State<ConversationList> {
showBack: false,
backgroundColor: Colors.transparent,
),
body: Padding(
padding: const EdgeInsets.only(top: 16,left: 16,right: 16),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
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: <Widget>[
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<Friend> 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<Friend> 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<ConversationList> {
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<void> _refresh() async {
Future.delayed(
const Duration(seconds: 1),
() {
print('Doing thing');
}
);
}


+ 63
- 75
mobile/lib/views/main/friend/list.dart View File

@ -11,13 +11,11 @@ import '/views/main/friend/request_list_item.dart';
class FriendList extends StatefulWidget {
final List<Friend> friends;
final List<Friend> 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<FriendList> {
List<Friend> friends = [];
List<Friend> friendRequests = [];
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
List<Friend> friends = [];
List<Friend> friendsDuplicate = [];
List<Friend> friendRequestsDuplicate = [];
@override
Widget build(BuildContext context) {
@ -46,31 +43,33 @@ class _FriendListState extends State<FriendList> {
showBack: false,
backgroundColor: Colors.transparent,
),
body: Padding(
padding: const EdgeInsets.only(top: 16,left: 16,right: 16),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
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: <Widget>[
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<FriendList> {
icon: const Icon(Icons.search, size: 25),
),
],
)
)
)
);
}
void filterSearchResults(String query) {
List<Friend> dummyFriendsList = [];
List<Friend> dummyFriendRequestsList = [];
dummyFriendsList.addAll(friends);
dummyFriendRequestsList.addAll(friendRequests);
if (query.isNotEmpty) {
List<Friend> dummyFriendData = [];
List<Friend> dummyFriendRequestData = [];
for (Friend item in dummyFriendsList) {
if(item.username.toLowerCase().contains(query)) {
@ -117,27 +113,17 @@ class _FriendListState extends State<FriendList> {
}
}
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<FriendList> {
void initState() {
super.initState();
friends.addAll(widget.friends);
friendRequests.addAll(widget.friendRequests);
}
Future<void> 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<FriendList> {
);
}
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<void> _refresh() async {
Future.delayed(
const Duration(seconds: 1),
() {
print('Doing thing');
}
);
}
}

+ 3
- 1
mobile/lib/views/main/friend/list_item.dart View File

@ -26,7 +26,9 @@ class _FriendListItemState extends State<FriendListItem> {
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(


+ 32
- 35
mobile/lib/views/main/home.dart View File

@ -35,7 +35,7 @@ class _HomeState extends State<Home> {
int _selectedIndex = 0;
List<Widget> _widgetOptions = <Widget>[
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<Home> {
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<Home> {
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<Home> {
),
FriendList(
friends: friends,
friendRequests: friendRequests,
callback: reinitDatabaseRecords,
),
Profile(profile: profile),
@ -184,8 +183,7 @@ class _HomeState extends State<Home> {
Future<void> 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<Home> {
),
FriendList(
friends: friends,
friendRequests: friendRequests,
callback: reinitDatabaseRecords,
),
Profile(profile: profile),


Loading…
Cancel
Save