Subscribe/unsubscribe from channels
This commit is contained in:
@@ -4,7 +4,6 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
@@ -154,8 +153,13 @@ class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
|
||||
itemBuilder: (context, item, index) => ChannelListItem(
|
||||
channel: item.channel,
|
||||
subscription: item.subscription,
|
||||
onPressed: () {
|
||||
Navi.push(context, () => ChannelViewPage(channelID: item.channel.channelID, preloadedData: (item.channel, item.subscription), needsReload: _enqueueReload));
|
||||
mode: ChannelListItemMode.Messages,
|
||||
onChannelListReloadTrigger: _enqueueReload,
|
||||
onSubscriptionChanged: (channelID, subscription) {
|
||||
setState(() {
|
||||
final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID);
|
||||
if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
151
flutter/lib/pages/channel_list/channel_list_extended.dart
Normal file
151
flutter/lib/pages/channel_list/channel_list_extended.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
class ChannelListExtendedPage extends StatefulWidget {
|
||||
const ChannelListExtendedPage({super.key});
|
||||
|
||||
@override
|
||||
State<ChannelListExtendedPage> createState() => _ChannelListExtendedPageState();
|
||||
}
|
||||
|
||||
class _ChannelListExtendedPageState extends State<ChannelListExtendedPage> with RouteAware {
|
||||
final PagingController<int, ChannelWithSubscription> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
|
||||
|
||||
bool _reloadEnqueued = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener(_fetchPage);
|
||||
|
||||
_pagingController.refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
Navi.modalRouteObserver.subscribe(this, ModalRoute.of(context)!);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ApplicationLog.debug('ChannelRootPage::dispose');
|
||||
_pagingController.dispose();
|
||||
Navi.modalRouteObserver.unsubscribe(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
if (_reloadEnqueued) {
|
||||
ApplicationLog.debug('[ChannelList::RouteObserver] --> didPopNext (will background-refresh) (_reloadEnqueued == true)');
|
||||
() async {
|
||||
_reloadEnqueued = false;
|
||||
await Future.delayed(const Duration(milliseconds: 500), () {}); // prevents flutter bug where the whole process crashes ?!?
|
||||
await _backgroundRefresh();
|
||||
}();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(int pageKey) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start ChannelList::_pagingController::_fetchPage [ ${pageKey} ]');
|
||||
|
||||
if (!acc.isAuth()) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final items = (await APIClient.getChannelList(acc, ChannelSelector.all)).toList();
|
||||
|
||||
items.sort((a, b) => -1 * (a.channel.timestampLastSent ?? '').compareTo(b.channel.timestampLastSent ?? ''));
|
||||
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
|
||||
} catch (exc, trace) {
|
||||
_pagingController.error = exc.toString();
|
||||
ApplicationLog.error('Failed to list channels: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _backgroundRefresh() async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start background refresh of channel list');
|
||||
|
||||
if (!acc.isAuth()) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
final items = (await APIClient.getChannelList(acc, ChannelSelector.all)).toList();
|
||||
|
||||
items.sort((a, b) => -1 * (a.channel.timestampLastSent ?? '').compareTo(b.channel.timestampLastSent ?? ''));
|
||||
|
||||
setState(() {
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
|
||||
});
|
||||
} catch (exc, trace) {
|
||||
setState(() {
|
||||
_pagingController.error = exc.toString();
|
||||
});
|
||||
ApplicationLog.error('Failed to list channels: ' + exc.toString(), trace: trace);
|
||||
} finally {
|
||||
AppBarState().setLoadingIndeterminate(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SCNScaffold(
|
||||
title: "Channels",
|
||||
showSearch: false,
|
||||
showShare: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(
|
||||
() => _pagingController.refresh(),
|
||||
),
|
||||
child: PagedListView<int, ChannelWithSubscription>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate<ChannelWithSubscription>(
|
||||
itemBuilder: (context, item, index) => ChannelListItem(
|
||||
channel: item.channel,
|
||||
subscription: item.subscription,
|
||||
mode: ChannelListItemMode.Extended,
|
||||
onChannelListReloadTrigger: _enqueueReload,
|
||||
onSubscriptionChanged: (channelID, subscription) {
|
||||
setState(() {
|
||||
final idx = _pagingController.itemList?.indexWhere((p) => p.channel.channelID == channelID);
|
||||
if (idx != null && idx >= 0) _pagingController.itemList![idx] = ChannelWithSubscription(channel: _pagingController.itemList![idx].channel, subscription: subscription);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _enqueueReload() {
|
||||
_reloadEnqueued = true;
|
||||
}
|
||||
}
|
@@ -7,23 +7,35 @@ import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/scn_message.dart';
|
||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
|
||||
enum ChannelListItemMode {
|
||||
Messages,
|
||||
Extended,
|
||||
}
|
||||
|
||||
class ChannelListItem extends StatefulWidget {
|
||||
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||
|
||||
const ChannelListItem({
|
||||
required this.channel,
|
||||
required this.onPressed,
|
||||
required this.onChannelListReloadTrigger,
|
||||
required this.onSubscriptionChanged,
|
||||
required this.subscription,
|
||||
required this.mode,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Channel channel;
|
||||
final Subscription? subscription;
|
||||
final Null Function() onPressed;
|
||||
final void Function() onChannelListReloadTrigger;
|
||||
final ChannelListItemMode mode;
|
||||
final void Function(String, Subscription?) onSubscriptionChanged;
|
||||
|
||||
@override
|
||||
State<ChannelListItem> createState() => _ChannelListItemState();
|
||||
@@ -38,11 +50,11 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
||||
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
if (acc.isAuth()) {
|
||||
if (acc.isAuth() && widget.mode == ChannelListItemMode.Messages) {
|
||||
lastMessage = SCNDataCache().getMessagesSorted().where((p) => p.channelID == widget.channel.channelID).firstOrNull;
|
||||
|
||||
() async {
|
||||
final (_, channelMessages) = await APIClient.getMessageList(acc, '@start', pageSize: 1, filter: MessageFilter(channelIDs: [widget.channel.channelID]));
|
||||
final (_, channelMessages) = await APIClient.getChannelMessageList(acc, widget.channel.channelID, '@start', pageSize: 1);
|
||||
setState(() {
|
||||
lastMessage = channelMessages.firstOrNull;
|
||||
});
|
||||
@@ -52,13 +64,18 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//TODO subscription status
|
||||
return Card.filled(
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
|
||||
color: Theme.of(context).cardTheme.color,
|
||||
child: InkWell(
|
||||
onTap: widget.onPressed,
|
||||
onTap: () {
|
||||
if (widget.mode == ChannelListItemMode.Messages) {
|
||||
Navi.push(context, () => ChannelMessageViewPage(channel: widget.channel));
|
||||
} else {
|
||||
Navi.push(context, () => ChannelViewPage(channelID: widget.channel.channelID, preloadedData: (widget.channel, widget.subscription), needsReload: widget.onChannelListReloadTrigger));
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
@@ -87,13 +104,8 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_preformatTitle(lastMessage),
|
||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
||||
),
|
||||
),
|
||||
Text(widget.channel.messagesSent.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
||||
Expanded(child: (widget.mode == ChannelListItemMode.Messages) ? Text(_preformatTitle(lastMessage), style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160))) : _buildSubscriptionStateText(context)),
|
||||
(widget.mode == ChannelListItemMode.Messages) ? Text(widget.channel.messagesSent.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)) : Text("", style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -102,11 +114,15 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
||||
SizedBox(width: 4),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navi.push(context, () => ChannelMessageViewPage(channel: this.widget.channel));
|
||||
if (widget.mode == ChannelListItemMode.Messages) {
|
||||
Navi.push(context, () => ChannelViewPage(channelID: widget.channel.channelID, preloadedData: (widget.channel, widget.subscription), needsReload: widget.onChannelListReloadTrigger));
|
||||
} else {
|
||||
Navi.push(context, () => ChannelMessageViewPage(channel: widget.channel));
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(FontAwesomeIcons.solidEnvelopes, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24),
|
||||
child: (widget.mode == ChannelListItemMode.Messages) ? Icon(FontAwesomeIcons.solidSquareInfo, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24) : Icon(FontAwesomeIcons.solidEnvelopes, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -123,13 +139,73 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
||||
|
||||
Widget _buildIcon(BuildContext context) {
|
||||
if (widget.subscription == null) {
|
||||
return Icon(FontAwesomeIcons.solidSquareDashed, color: Theme.of(context).colorScheme.outline, size: 32); // not-subscribed
|
||||
Widget result = Icon(FontAwesomeIcons.solidSquareDashed, color: Theme.of(context).colorScheme.outline, size: 32); // not-subscribed
|
||||
result = GestureDetector(onTap: () => _subscribe(), child: result);
|
||||
return result;
|
||||
} else if (widget.subscription!.confirmed && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
||||
return Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (own channel)
|
||||
Widget result = Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (own channel)
|
||||
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
|
||||
return result;
|
||||
} else if (widget.subscription!.confirmed) {
|
||||
return Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (foreign channel)
|
||||
Widget result = Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (foreign channel)
|
||||
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
|
||||
return result;
|
||||
} else {
|
||||
return Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32); // requested
|
||||
Widget result = Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32); // requested
|
||||
result = GestureDetector(onTap: () => _unsubscribe(widget.subscription!), child: result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSubscriptionStateText(BuildContext context) {
|
||||
if (widget.subscription == null) {
|
||||
return Text("", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||
} else if (widget.subscription!.confirmed && widget.channel.ownerUserID == widget.subscription!.subscriberUserID) {
|
||||
return Text("subscribed", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||
} else if (widget.subscription!.confirmed) {
|
||||
return Text("subscripted (foreign channe)", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||
} else {
|
||||
return Text("subscription requested", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)));
|
||||
}
|
||||
}
|
||||
|
||||
void _subscribe() async {
|
||||
final acc = AppAuth();
|
||||
|
||||
if (acc.isAuth() && widget.channel.ownerUserID == acc.getUserID()) {
|
||||
try {
|
||||
var sub = await APIClient.subscribeToChannelbyID(acc, widget.channel.channelID);
|
||||
widget.onChannelListReloadTrigger.call();
|
||||
|
||||
widget.onSubscriptionChanged(widget.channel.channelID, sub);
|
||||
|
||||
if (sub.confirmed) {
|
||||
Toaster.success("Success", 'Subscribed to channel');
|
||||
} else {
|
||||
Toaster.success("Success", 'Requested widget.subscription to channel');
|
||||
}
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to subscribe to channel');
|
||||
ApplicationLog.error('Failed to subscribe to channel: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _unsubscribe(Subscription sub) async {
|
||||
final acc = AppAuth();
|
||||
|
||||
if (acc.isAuth() && widget.channel.ownerUserID == acc.getUserID() && widget.subscription != null) {
|
||||
try {
|
||||
await APIClient.deleteSubscription(acc, widget.channel.channelID, widget.subscription!.subscriptionID);
|
||||
widget.onChannelListReloadTrigger.call();
|
||||
|
||||
widget.onSubscriptionChanged?.call(widget.channel.channelID, null);
|
||||
|
||||
Toaster.success("Success", 'Unsubscribed from channel');
|
||||
} catch (exc, trace) {
|
||||
Toaster.error("Error", 'Failed to unsubscribe from channel');
|
||||
ApplicationLog.error('Failed to unsubscribe from channel: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user