2024-02-10 19:57:17 +01:00
import ' package:flutter/material.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 ' ;
2024-06-15 21:29:51 +02:00
import ' package:simplecloudnotifier/models/scn_message.dart ' ;
2024-06-16 00:46:46 +02:00
import ' package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart ' ;
2024-05-21 23:20:34 +02:00
import ' package:simplecloudnotifier/pages/message_view/message_view.dart ' ;
2024-06-15 21:29:51 +02:00
import ' package:simplecloudnotifier/settings/app_settings.dart ' ;
2024-06-15 15:56:50 +02:00
import ' package:simplecloudnotifier/state/app_bar_state.dart ' ;
2024-06-17 23:23:35 +02:00
import ' package:simplecloudnotifier/state/app_events.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-17 23:23:35 +02:00
import ' package:simplecloudnotifier/state/scn_data_cache.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
@ override
State < MessageListPage > createState ( ) = > _MessageListPageState ( ) ;
}
2024-06-15 15:56:50 +02:00
class _MessageListPageState extends State < MessageListPage > with RouteAware {
2024-06-15 17:19:23 +02:00
late final AppLifecycleListener _lifecyleListener ;
2024-06-15 21:29:51 +02:00
PagingController < String , SCNMessage > _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-06-16 00:46:46 +02:00
List < MessageFilterChiplet > _filterChiplets = [ ] ;
2024-02-18 17:36:58 +01:00
@ override
void initState ( ) {
super . initState ( ) ;
2024-06-15 15:56:50 +02:00
2024-09-19 19:46:46 +02:00
AppEvents ( ) . subscribeFilterListener ( _onAddFilter ) ;
2024-06-17 23:23:35 +02:00
AppEvents ( ) . subscribeMessageReceivedListener ( _onMessageReceivedViaNotification ) ;
2024-06-16 00:46:46 +02:00
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-17 23:23:35 +02:00
if ( SCNDataCache ( ) . hasMessagesAndChannels ( ) ) {
2024-06-15 15:56:50 +02:00
// ==== Use cache values - and refresh in background
2024-06-17 23:23:35 +02:00
_channels = SCNDataCache ( ) . getChannelMap ( ) ;
2024-06-15 15:56:50 +02:00
2024-10-19 22:33:08 +02:00
//TODO this is not 100% correct - the message-cache contains (which is right!) all messages, even from unsubscribed channels
//TODO what we should do is save another list in SCNDataCache, with the result of the last getMessageList call (page-1) and use that
//TODO this way we only get 1 page of data from cache, but its a weird behaviour anway that we loose data once _backgroundRefresh is finished
2024-06-17 23:23:35 +02:00
_pagingController . value = PagingState ( nextPageKey: null , itemList: SCNDataCache ( ) . getMessagesSorted ( ) , error: null ) ;
2024-06-15 15:56:50 +02:00
_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-09-19 19:46:46 +02:00
AppEvents ( ) . unsubscribeFilterListener ( _onAddFilter ) ;
2024-06-17 23:23:35 +02:00
AppEvents ( ) . unsubscribeMessageReceivedListener ( _onMessageReceivedViaNotification ) ;
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-18 17:36:41 +02:00
if ( AppSettings ( ) . backgroundRefreshMessageListOnPop ) {
2024-06-17 22:54:45 +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 ( ) {
2024-06-25 12:00:34 +02:00
if ( AppSettings ( ) . alwaysBackgroundRefreshMessageListOnLifecycleResume & & widget . isVisiblePage ) {
2024-06-18 17:36:41 +02:00
ApplicationLog . debug ( ' [MessageList::_onLifecycleResume] --> (will background-refresh) ' ) ;
_backgroundRefresh ( false ) ;
}
2024-06-15 17:19:23 +02:00
}
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-06-15 21:29:51 +02:00
final cfg = Provider . of < AppSettings > ( 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
2024-06-17 23:23:35 +02:00
SCNDataCache ( ) . setChannelCache ( channels ) ; // no await
2024-05-21 23:20:34 +02:00
}
2024-09-19 19:46:46 +02:00
final ( npt , newItems ) = await APIClient . getMessageList ( acc , thisPageToken , pageSize: cfg . messagePageSize , filter: _getFilter ( ) ) ;
2024-02-18 17:36:58 +01:00
2024-06-17 23:23:35 +02:00
SCNDataCache ( ) . addToMessageCache ( newItems ) ; // no await
2024-06-15 15:56:50 +02:00
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 ) ;
2024-06-15 21:29:51 +02:00
final cfg = Provider . of < AppSettings > ( context , listen: false ) ;
2024-06-15 15:56:50 +02:00
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 } ;
} ) ;
2024-06-17 23:23:35 +02:00
SCNDataCache ( ) . setChannelCache ( channels ) ; // no await
2024-06-15 15:56:50 +02:00
}
2024-06-15 21:29:51 +02:00
final ( npt , newItems ) = await APIClient . getMessageList ( acc , ' @start ' , pageSize: cfg . messagePageSize ) ;
2024-06-15 15:56:50 +02:00
2024-06-17 23:23:35 +02:00
SCNDataCache ( ) . addToMessageCache ( newItems ) ; // no await
2024-06-15 15:56:50 +02:00
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-06-16 00:46:46 +02:00
child: Column (
crossAxisAlignment: CrossAxisAlignment . stretch ,
children: [
if ( _filterChiplets . isNotEmpty )
Wrap (
alignment: WrapAlignment . start ,
spacing: 5.0 ,
children: [
for ( var chiplet in _filterChiplets ) _buildFilterChip ( context , chiplet ) ,
] ,
) ,
Expanded (
child: RefreshIndicator (
onRefresh: ( ) = > Future . sync (
( ) = > _pagingController . refresh ( ) ,
) ,
child: PagedListView < String , SCNMessage > (
pagingController: _pagingController ,
builderDelegate: PagedChildBuilderDelegate < SCNMessage > (
itemBuilder: ( context , item , index ) = > MessageListItem (
message: item ,
allChannels: _channels ? ? { } ,
onPressed: ( ) {
2024-07-13 01:05:32 +02:00
Navi . push ( context , ( ) = > MessageViewPage ( messageID: item . messageID , preloadedData: ( item , ) ) ) ;
2024-06-16 00:46:46 +02:00
} ,
) ,
) ,
) ,
2024-05-26 19:24:19 +02:00
) ,
2024-05-21 23:20:34 +02:00
) ,
2024-06-16 00:46:46 +02:00
] ,
) ,
) ;
}
Widget _buildFilterChip ( BuildContext context , MessageFilterChiplet chiplet ) {
return Padding (
padding: const EdgeInsets . fromLTRB ( 0 , 2 , 0 , 2 ) ,
child: InputChip (
avatar: Icon ( chiplet . icon ( ) ) ,
label: Text ( chiplet . label ) ,
2024-09-19 19:46:46 +02:00
onDeleted: ( ) = > _onRemFilter ( chiplet ) ,
2024-06-16 00:46:46 +02:00
onPressed: ( ) { /* TODO idk what to do here ? */ } ,
visualDensity: VisualDensity ( horizontal: - 4 , vertical: - 4 ) ,
2024-02-10 19:57:17 +01:00
) ,
) ;
}
2024-06-15 15:56:50 +02:00
2024-09-19 19:46:46 +02:00
void _onAddFilter ( List < MessageFilterChipletType > remTypeList , List < MessageFilterChiplet > chiplets ) {
2024-06-16 00:46:46 +02:00
setState ( ( ) {
2024-09-19 19:46:46 +02:00
final remTypes = remTypeList . toSet ( ) ;
_filterChiplets = _filterChiplets . where ( ( element ) = > ! remTypes . contains ( element . type ) ) . toList ( ) + chiplets ;
_pagingController . refresh ( ) ;
} ) ;
}
void _onRemFilter ( MessageFilterChiplet chiplet ) {
setState ( ( ) {
_filterChiplets . remove ( chiplet ) ;
_pagingController . refresh ( ) ;
2024-06-16 00:46:46 +02:00
} ) ;
}
2024-06-17 23:23:35 +02:00
void _onMessageReceivedViaNotification ( SCNMessage msg ) {
setState ( ( ) {
_pagingController . itemList = [ msg ] + ( _pagingController . itemList ? ? [ ] ) ;
} ) ;
}
2024-09-19 19:46:46 +02:00
MessageFilter _getFilter ( ) {
var filter = MessageFilter ( ) ;
var chipletsChannel = _filterChiplets . where ( ( p ) = > p . type = = MessageFilterChipletType . channel ) . toList ( ) ;
if ( chipletsChannel . isNotEmpty ) {
filter . channelIDs = chipletsChannel . map ( ( p ) = > p . value as String ) . toList ( ) ;
}
var chipletsSearch = _filterChiplets . where ( ( p ) = > p . type = = MessageFilterChipletType . search ) . toList ( ) ;
if ( chipletsSearch . isNotEmpty ) {
2024-09-20 20:37:55 +02:00
filter . searchFilter = chipletsSearch . map ( ( p ) = > p . value as String ) . toList ( ) ;
2024-09-19 19:46:46 +02:00
}
var chipletsKeyTokens = _filterChiplets . where ( ( p ) = > p . type = = MessageFilterChipletType . sendkey ) . toList ( ) ;
if ( chipletsKeyTokens . isNotEmpty ) {
filter . usedKeys = chipletsKeyTokens . map ( ( p ) = > p . value as String ) . toList ( ) ;
}
var chipletPriority = _filterChiplets . where ( ( p ) = > p . type = = MessageFilterChipletType . priority ) . toList ( ) ;
if ( chipletPriority . isNotEmpty ) {
filter . priority = chipletPriority . map ( ( p ) = > p . value as int ) . toList ( ) ;
}
var chipletSender = _filterChiplets . where ( ( p ) = > p . type = = MessageFilterChipletType . sender ) . toList ( ) ;
if ( chipletSender . isNotEmpty ) {
filter . senderNames = chipletSender . map ( ( p ) = > p . value as String ) . toList ( ) ;
}
return filter ;
}
2024-02-10 19:57:17 +01:00
}