2024-02-10 19:57:17 +01:00
import ' package:flutter/material.dart ' ;
2024-06-15 15:56:50 +02:00
import ' package:hive_flutter/hive_flutter.dart ' ;
2024-02-18 17:36:58 +01:00
import ' package:infinite_scroll_pagination/infinite_scroll_pagination.dart ' ;
import ' package:provider/provider.dart ' ;
import ' package:simplecloudnotifier/api/api_client.dart ' ;
2024-05-21 23:20:34 +02:00
import ' package:simplecloudnotifier/models/channel.dart ' ;
import ' package:simplecloudnotifier/models/message.dart ' ;
import ' package:simplecloudnotifier/pages/message_view/message_view.dart ' ;
2024-06-15 15:56:50 +02:00
import ' package:simplecloudnotifier/state/app_bar_state.dart ' ;
2024-05-26 00:20:25 +02:00
import ' package:simplecloudnotifier/state/application_log.dart ' ;
2024-06-02 17:09:57 +02:00
import ' package:simplecloudnotifier/state/app_auth.dart ' ;
2024-05-25 22:06:43 +02:00
import ' package:simplecloudnotifier/pages/message_list/message_list_item.dart ' ;
2024-06-13 15:42:39 +02:00
import ' package:simplecloudnotifier/utils/navi.dart ' ;
2024-02-10 19:57:17 +01:00
2024-02-18 17:36:58 +01:00
class MessageListPage extends StatefulWidget {
2024-06-15 15:56:50 +02:00
const MessageListPage ( { super . key , required this . isVisiblePage } ) ;
final bool isVisiblePage ;
2024-02-18 17:36:58 +01:00
2024-06-04 08:20:28 +02:00
//TODO reload on switch to tab
//TODO reload on app to foreground
2024-02-18 17:36:58 +01:00
@ override
State < MessageListPage > createState ( ) = > _MessageListPageState ( ) ;
}
2024-06-15 15:56:50 +02:00
class _MessageListPageState extends State < MessageListPage > with RouteAware {
2024-05-21 23:20:34 +02:00
static const _pageSize = 128 ;
2024-02-18 17:36:58 +01:00
2024-06-15 17:19:23 +02:00
late final AppLifecycleListener _lifecyleListener ;
2024-06-15 15:56:50 +02:00
PagingController < String , Message > _pagingController = PagingController . fromValue ( PagingState ( nextPageKey: null , itemList: [ ] , error: null ) , firstPageKey: ' @start ' ) ;
2024-02-18 17:36:58 +01:00
2024-05-21 23:20:34 +02:00
Map < String , Channel > ? _channels = null ;
2024-06-15 15:56:50 +02:00
bool _isInitialized = false ;
2024-02-18 17:36:58 +01:00
@ override
void initState ( ) {
super . initState ( ) ;
2024-06-15 15:56:50 +02:00
_pagingController . addPageRequestListener ( _fetchPage ) ;
2024-06-15 16:33:30 +02:00
if ( widget . isVisiblePage & & ! _isInitialized ) _realInitState ( ) ;
2024-06-15 17:19:23 +02:00
_lifecyleListener = AppLifecycleListener (
onResume: _onLifecycleResume ,
) ;
2024-06-15 15:56:50 +02:00
}
@ override
void didUpdateWidget ( MessageListPage oldWidget ) {
super . didUpdateWidget ( oldWidget ) ;
if ( oldWidget . isVisiblePage ! = widget . isVisiblePage & & widget . isVisiblePage ) {
if ( ! _isInitialized ) {
2024-06-15 16:33:30 +02:00
_realInitState ( ) ;
2024-06-15 15:56:50 +02:00
} else {
_backgroundRefresh ( false ) ;
}
}
}
2024-06-15 16:33:30 +02:00
void _realInitState ( ) {
ApplicationLog . debug ( ' MessageListPage::_realInitState ' ) ;
2024-06-15 15:56:50 +02:00
final chnCache = Hive . box < Channel > ( ' scn-channel-cache ' ) ;
final msgCache = Hive . box < Message > ( ' scn-message-cache ' ) ;
if ( chnCache . isNotEmpty & & msgCache . isNotEmpty ) {
// ==== Use cache values - and refresh in background
_channels = < String , Channel > { for ( var v in chnCache . values ) v . channelID: v } ;
final cacheMessages = msgCache . values . toList ( ) ;
cacheMessages . sort ( ( a , b ) = > - 1 * a . timestamp . compareTo ( b . timestamp ) ) ;
_pagingController . value = PagingState ( nextPageKey: null , itemList: cacheMessages , error: null ) ;
_backgroundRefresh ( true ) ;
} else {
// ==== Full refresh - no cache available
_pagingController . refresh ( ) ;
}
_isInitialized = true ;
}
@ override
void didChangeDependencies ( ) {
super . didChangeDependencies ( ) ;
Navi . modalRouteObserver . subscribe ( this , ModalRoute . of ( context ) ! ) ;
2024-02-18 17:36:58 +01:00
}
@ override
void dispose ( ) {
2024-06-15 16:33:30 +02:00
ApplicationLog . debug ( ' MessageListPage::dispose ' ) ;
2024-06-15 15:56:50 +02:00
Navi . modalRouteObserver . unsubscribe ( this ) ;
2024-02-18 17:36:58 +01:00
_pagingController . dispose ( ) ;
2024-06-15 17:19:23 +02:00
_lifecyleListener . dispose ( ) ;
2024-02-18 17:36:58 +01:00
super . dispose ( ) ;
}
2024-06-15 15:56:50 +02:00
@ override
void didPush ( ) {
2024-06-15 16:33:30 +02:00
// ...
2024-06-15 15:56:50 +02:00
}
@ override
void didPopNext ( ) {
2024-06-15 16:33:30 +02:00
ApplicationLog . debug ( ' [MessageList::RouteObserver] --> didPopNext (will background-refresh) ' ) ;
_backgroundRefresh ( false ) ;
2024-06-15 15:56:50 +02:00
}
2024-06-15 17:19:23 +02:00
void _onLifecycleResume ( ) {
ApplicationLog . debug ( ' [MessageList::_onLifecycleResume] --> (will background-refresh) ' ) ;
_backgroundRefresh ( false ) ;
}
2024-02-18 17:36:58 +01:00
Future < void > _fetchPage ( String thisPageToken ) async {
2024-06-02 17:09:57 +02:00
final acc = Provider . of < AppAuth > ( context , listen: false ) ;
2024-02-18 17:36:58 +01:00
2024-06-15 15:56:50 +02:00
ApplicationLog . debug ( ' Start MessageList::_pagingController::_fetchPage [ ${ thisPageToken } ] ' ) ;
2024-06-02 17:09:57 +02:00
if ( ! acc . isAuth ( ) ) {
2024-02-18 17:36:58 +01:00
_pagingController . error = ' Not logged in ' ;
return ;
}
try {
2024-05-21 23:20:34 +02:00
if ( _channels = = null ) {
2024-06-02 17:09:57 +02:00
final channels = await APIClient . getChannelList ( acc , ChannelSelector . allAny ) ;
2024-06-01 03:06:02 +02:00
_channels = < String , Channel > { for ( var v in channels ) v . channel . channelID: v . channel } ;
2024-06-15 15:56:50 +02:00
_setChannelCache ( channels ) ; // no await
2024-05-21 23:20:34 +02:00
}
2024-06-02 17:09:57 +02:00
final ( npt , newItems ) = await APIClient . getMessageList ( acc , thisPageToken , pageSize: _pageSize ) ;
2024-02-18 17:36:58 +01:00
2024-06-15 15:56:50 +02:00
_addToMessageCache ( newItems ) ; // no await
ApplicationLog . debug ( ' Finished MessageList::_pagingController::_fetchPage [ ${ newItems . length } items and npt: ${ thisPageToken } --> ${ npt } ] ' ) ;
2024-02-18 17:36:58 +01:00
if ( npt = = ' @end ' ) {
_pagingController . appendLastPage ( newItems ) ;
} else {
_pagingController . appendPage ( newItems , npt ) ;
}
2024-05-26 00:20:25 +02:00
} catch ( exc , trace ) {
_pagingController . error = exc . toString ( ) ;
ApplicationLog . error ( ' Failed to list messages: ' + exc . toString ( ) , trace: trace ) ;
2024-02-18 17:36:58 +01:00
}
}
2024-02-10 19:57:17 +01:00
2024-06-15 15:56:50 +02:00
Future < void > _backgroundRefresh ( bool fullReplaceState ) async {
final acc = Provider . of < AppAuth > ( context , listen: false ) ;
ApplicationLog . debug ( ' Start background refresh of message list (fullReplaceState: $ fullReplaceState ) ' ) ;
try {
await Future . delayed ( const Duration ( seconds: 0 ) , ( ) { } ) ; // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
AppBarState ( ) . setLoadingIndeterminate ( true ) ;
if ( _channels = = null | | fullReplaceState ) {
final channels = await APIClient . getChannelList ( acc , ChannelSelector . allAny ) ;
setState ( ( ) {
_channels = < String , Channel > { for ( var v in channels ) v . channel . channelID: v . channel } ;
} ) ;
_setChannelCache ( channels ) ; // no await
}
final ( npt , newItems ) = await APIClient . getMessageList ( acc , ' @start ' , pageSize: _pageSize ) ;
_addToMessageCache ( newItems ) ; // no await
if ( fullReplaceState ) {
// fully replace/reset state
ApplicationLog . debug ( ' Background-refresh finished (fullReplaceState) - replace state with ${ newItems . length } items and npt: [ $ npt ] ' ) ;
setState ( ( ) {
if ( npt = = ' @end ' )
_pagingController . value = PagingState ( nextPageKey: null , itemList: newItems , error: null ) ;
else
_pagingController . value = PagingState ( nextPageKey: npt , itemList: newItems , error: null ) ;
} ) ;
} else {
final itemsToBeAdded = newItems . where ( ( p1 ) = > ! ( _pagingController . itemList ? ? [ ] ) . any ( ( p2 ) = > p1 . messageID = = p2 . messageID ) ) . toList ( ) ;
if ( itemsToBeAdded . isEmpty ) {
// nothing to do - no new items...
// ....
ApplicationLog . debug ( ' Background-refresh returned no new items - nothing to do. ' ) ;
} else if ( itemsToBeAdded . length = = newItems . length ) {
// all items are new ?!?, the current state is completely fucked - full replace
ApplicationLog . debug ( ' Background-refresh found only new items ?!? - fully replace state with ${ newItems . length } items ' ) ;
setState ( ( ) {
if ( npt = = ' @end ' )
_pagingController . value = PagingState ( nextPageKey: null , itemList: newItems , error: null ) ;
else
_pagingController . value = PagingState ( nextPageKey: npt , itemList: newItems , error: null ) ;
_pagingController . itemList = null ;
} ) ;
} else {
// add new items to the front
ApplicationLog . debug ( ' Background-refresh found ${ newItems . length } new items - add to front ' ) ;
setState ( ( ) {
_pagingController . itemList = itemsToBeAdded + ( _pagingController . itemList ? ? [ ] ) ;
} ) ;
}
}
} catch ( exc , trace ) {
setState ( ( ) {
_pagingController . error = exc . toString ( ) ;
} ) ;
ApplicationLog . error ( ' Failed to list messages: ' + exc . toString ( ) , trace: trace ) ;
} finally {
AppBarState ( ) . setLoadingIndeterminate ( false ) ;
}
}
2024-02-10 19:57:17 +01:00
@ override
Widget build ( BuildContext context ) {
2024-05-21 23:20:34 +02:00
return Padding (
padding: EdgeInsets . fromLTRB ( 8 , 4 , 8 , 4 ) ,
2024-05-26 19:24:19 +02:00
child: RefreshIndicator (
onRefresh: ( ) = > Future . sync (
( ) = > _pagingController . refresh ( ) ,
) ,
child: PagedListView < String , Message > (
pagingController: _pagingController ,
builderDelegate: PagedChildBuilderDelegate < Message > (
itemBuilder: ( context , item , index ) = > MessageListItem (
message: item ,
allChannels: _channels ? ? { } ,
onPressed: ( ) {
2024-06-13 15:42:39 +02:00
Navi . push ( context , ( ) = > MessageViewPage ( message: item ) ) ;
2024-05-26 19:24:19 +02:00
} ,
) ,
2024-05-21 23:20:34 +02:00
) ,
2024-02-10 19:57:17 +01:00
) ,
) ,
) ;
}
2024-06-15 15:56:50 +02:00
Future < void > _setChannelCache ( List < ChannelWithSubscription > channels ) async {
final cache = Hive . box < Channel > ( ' scn-channel-cache ' ) ;
if ( cache . length ! = channels . length ) await cache . clear ( ) ;
for ( var chn in channels ) await cache . put ( chn . channel . channelID , chn . channel ) ;
}
Future < void > _addToMessageCache ( List < Message > newItems ) async {
final cache = Hive . box < Message > ( ' scn-message-cache ' ) ;
for ( var msg in newItems ) await cache . put ( msg . messageID , msg ) ;
// delete all but the newest 128 messages
if ( cache . length < _pageSize ) return ;
final allValues = cache . values . toList ( ) ;
allValues . sort ( ( a , b ) = > - 1 * a . timestamp . compareTo ( b . timestamp ) ) ;
for ( var val in allValues . sublist ( _pageSize ) ) {
await cache . delete ( val . messageID ) ;
}
}
2024-02-10 19:57:17 +01:00
}