Subscribe/unsubscribe from channels

This commit is contained in:
Mike Schwörer 2024-10-19 19:42:05 +02:00
parent 9b2e429d3d
commit 1cf14e65a9
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
13 changed files with 468 additions and 60 deletions

View File

@ -5,6 +5,7 @@ import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/models/api_error.dart';
import 'package:simplecloudnotifier/models/client.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/models/sender_name_statistics.dart';
import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
@ -101,19 +102,21 @@ class APIClient {
}
if (responseStatusCode != 200) {
try {
final apierr = APIError.fromJson(jsonDecode(responseBody) as Map<String, dynamic>);
APIError apierr;
RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr);
Toaster.error("Error", 'Request "${name}" failed');
throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message);
try {
apierr = APIError.fromJson(jsonDecode(responseBody) as Map<String, dynamic>);
} catch (exc, trace) {
ApplicationLog.warn('Failed to decode api response as error-object', additional: exc.toString() + "\nBody:\n" + responseBody, trace: trace);
RequestLog.addRequestErrorStatuscode(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders);
Toaster.error("Error", 'Request "${name}" failed');
throw Exception('API request failed with status code ${responseStatusCode}');
}
RequestLog.addRequestErrorStatuscode(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders);
Toaster.error("Error", 'Request "${name}" failed');
throw Exception('API request failed with status code ${responseStatusCode}');
RequestLog.addRequestAPIError(name, t0, method, uri, req.body, req.headers, responseStatusCode, responseBody, responseHeaders, apierr);
Toaster.error("Error", apierr.message);
throw APIException(responseStatusCode, apierr.error, apierr.errhighlight, apierr.message);
}
try {
@ -281,6 +284,20 @@ class APIClient {
);
}
static Future<(String, List<SCNMessage>)> getChannelMessageList(TokenSource auth, String cid, String pageToken, {int? pageSize}) async {
return await _request(
name: 'getChannelMessageList',
method: 'GET',
relURL: 'users/${auth.getUserID()}/channels/${cid}/messages',
query: {
'next_page_token': [pageToken],
if (pageSize != null) 'page_size': [pageSize.toString()],
},
fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'),
authToken: auth.getToken(),
);
}
static Future<List<Subscription>> getSubscriptionList(TokenSource auth) async {
return await _request(
name: 'getSubscriptionList',
@ -369,7 +386,62 @@ class APIClient {
);
}
static Future<List<String>> getSenderNameList(AppAuth userAcc) {
return Future.value(['TODO']); //TODO
static Future<List<SenderNameStatistics>> getSenderNameList(TokenSource auth) async {
return await _request(
name: 'getSenderNameList',
method: 'GET',
relURL: 'users/${auth.getUserID()}/sender-names',
fn: (json) => SenderNameStatistics.fromJsonArray(json['sender_names'] as List<dynamic>),
authToken: auth.getToken(),
);
}
static Future<Subscription> subscribeToChannelbyID(TokenSource auth, String channelID) async {
return await _request(
name: 'subscribeToChannelbyID',
method: 'POST',
relURL: 'users/${auth.getUserID()}/subscriptions',
jsonBody: {
'channel_id': channelID,
},
fn: Subscription.fromJson,
authToken: auth.getToken(),
);
}
static Future<Subscription> deleteSubscription(TokenSource auth, String channelID, String subID) async {
return await _request(
name: 'deleteSubscription',
method: 'DELETE',
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
fn: Subscription.fromJson,
authToken: auth.getToken(),
);
}
static Future<Subscription> confirmSubscription(TokenSource auth, String channelID, String subID) async {
return await _request(
name: 'confirmSubscription',
method: 'PATCH',
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
jsonBody: {
'confirmed': true,
},
fn: Subscription.fromJson,
authToken: auth.getToken(),
);
}
static Future<Subscription> unconfirmSubscription(TokenSource auth, String channelID, String subID) async {
return await _request(
name: 'unconfirmSubscription',
method: 'PATCH',
relURL: 'users/${auth.getUserID()}/subscriptions/${subID}',
jsonBody: {
'confirmed': false,
},
fn: Subscription.fromJson,
authToken: auth.getToken(),
);
}
}

View File

@ -1,7 +1,7 @@
class APIException implements Exception {
final int httpStatus;
final int error;
final String errHighlight;
final int errHighlight;
final String message;
APIException(this.httpStatus, this.error, this.errHighlight, this.message);

View File

@ -25,7 +25,7 @@ class _FilterModalSendernameState extends State<FilterModalSendername> {
final userAcc = Provider.of<AppAuth>(context, listen: false);
if (!userAcc.isAuth()) throw new Exception('not logged in');
final senders = await APIClient.getSenderNameList(userAcc);
final senders = (await APIClient.getSenderNameList(userAcc)).map((p) => p.name).toList();
return senders;
}());

View File

@ -1,7 +1,7 @@
class APIError {
final bool success;
final int error;
final String errhighlight;
final int errhighlight;
final String message;
static final MISSING_UID = 1101;
@ -67,7 +67,7 @@ class APIError {
return APIError(
success: json['success'] as bool,
error: (json['error'] as num).toInt(),
errhighlight: json['errhighlight'] as String,
errhighlight: (json['errhighlight'] as num).toInt(),
message: json['message'] as String,
);
}

View File

@ -0,0 +1,26 @@
class SenderNameStatistics {
final String name;
final String ts_last;
final String ts_first;
final int count;
const SenderNameStatistics({
required this.name,
required this.ts_last,
required this.ts_first,
required this.count,
});
factory SenderNameStatistics.fromJson(Map<String, dynamic> json) {
return SenderNameStatistics(
name: json['name'] as String,
ts_last: json['ts_last'] as String,
ts_first: json['ts_first'] as String,
count: json['count'] as int,
);
}
static List<SenderNameStatistics> fromJsonArray(List<dynamic> jsonArr) {
return jsonArr.map<SenderNameStatistics>((e) => SenderNameStatistics.fromJson(e as Map<String, dynamic>)).toList();
}
}

View File

@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/pages/account/login.dart';
import 'package:simplecloudnotifier/pages/channel_list/channel_list_extended.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart';
@ -32,6 +33,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
late ImmediateFuture<int>? futureKeyCount;
late ImmediateFuture<int>? futureChannelAllCount;
late ImmediateFuture<int>? futureChannelSubscribedCount;
late ImmediateFuture<int>? futureSenderNamesCount;
late ImmediateFuture<User>? futureUser;
late AppAuth userAcc;
@ -87,6 +89,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
futureKeyCount = null;
futureChannelAllCount = null;
futureChannelSubscribedCount = null;
futureSenderNamesCount = null;
if (userAcc.isAuth()) {
futureChannelAllCount = ImmediateFuture.ofFuture(() async {
@ -119,6 +122,12 @@ class _AccountRootPageState extends State<AccountRootPage> {
return keys.length;
}());
futureSenderNamesCount = ImmediateFuture.ofFuture(() async {
if (!userAcc.isAuth()) throw new Exception('not logged in');
final senders = (await APIClient.getSenderNameList(userAcc)).map((p) => p.name).toList();
return senders.length;
}());
futureUser = ImmediateFuture.ofFuture(userAcc.loadUser(force: false));
}
}
@ -137,6 +146,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
final subs = await APIClient.getSubscriptionList(userAcc);
final clients = await APIClient.getClientList(userAcc);
final keys = await APIClient.getKeyTokenList(userAcc);
final senderNames = await APIClient.getSenderNameList(userAcc);
final user = await userAcc.loadUser(force: true);
setState(() {
@ -145,6 +155,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
futureSubscriptionCount = ImmediateFuture.ofValue(subs.length);
futureClientCount = ImmediateFuture.ofValue(clients.length);
futureKeyCount = ImmediateFuture.ofValue(keys.length);
futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length);
futureUser = ImmediateFuture.ofValue(user);
});
} catch (exc, trace) {
@ -368,7 +379,10 @@ class _AccountRootPageState extends State<AccountRootPage> {
_buildNumberCard(context, 'Subscriptions', futureSubscriptionCount, () {/*TODO*/}),
_buildNumberCard(context, 'Clients', futureClientCount, () {/*TODO*/}),
_buildNumberCard(context, 'Keys', futureKeyCount, () {/*TODO*/}),
_buildNumberCard(context, 'Channels', futureChannelSubscribedCount, () {/*TODO*/}),
_buildNumberCard(context, 'Channels', futureChannelSubscribedCount, () {
Navi.push(context, () => ChannelListExtendedPage());
}),
_buildNumberCard(context, 'Sender', futureSenderNamesCount, () {/*TODO*/}),
UI.buttonCard(
context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),

View File

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

View 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;
}
}

View File

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

View File

@ -55,7 +55,7 @@ class _ChannelMessageViewPageState extends State<ChannelMessageViewPage> {
}
try {
final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize, filter: MessageFilter(channelIDs: [this.widget.channel.channelID]));
final (npt, newItems) = await APIClient.getChannelMessageList(acc, this.widget.channel.channelID, thisPageToken, pageSize: cfg.messagePageSize);
SCNDataCache().addToMessageCache(newItems); // no await

View File

@ -63,15 +63,15 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
@override
void initState() {
_initStateAsync();
_initStateAsync(true);
super.initState();
}
void _initStateAsync() async {
Future<void> _initStateAsync(bool usePreload) async {
final userAcc = Provider.of<AppAuth>(context, listen: false);
if (widget.preloadedData != null) {
if (widget.preloadedData != null && usePreload) {
channelPreview = widget.preloadedData!.$1.toPreview();
channel = widget.preloadedData!.$1;
subscription = widget.preloadedData!.$2;
@ -231,7 +231,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidDiagramSubtask,
title: 'Subscription (own)',
title: 'Subscription (foreign)',
values: [_formatSubscriptionStatus(subscription)],
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, _subscribe)],
),
@ -296,7 +296,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
future: _futureSubscribeKey.future,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
var text = 'TODO' + '\n' + channel!.channelID + '\n' + snapshot.data!; //TODO deeplink-y (also perhaps just bas64 everything together?)
var text = '@scn.channel.subscribe' + '\n' + "v1" + '\n' + channel!.displayName + '\n' + channel!.ownerUserID + '\n' + channel!.channelID + '\n' + snapshot.data!;
return GestureDetector(
onTap: () {
Share.share(text, subject: _displayNameOverride ?? channel!.displayName);
@ -305,7 +305,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
child: QrImageView(
data: text,
version: QrVersions.auto,
size: 300.0,
size: 265.0,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: Theme.of(context).textTheme.bodyLarge?.color,
@ -446,14 +446,6 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
}
}
void _subscribe() {
//TODO
}
void _unsubscribe() {
//TODO
}
void _showEditDisplayName() {
setState(() {
_ctrlDisplayName.text = _displayNameOverride ?? channelPreview?.displayName ?? '';
@ -518,16 +510,90 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
}
}
void _cancelForeignSubscription(Subscription sub) {
//TODO
void _subscribe() async {
final acc = AppAuth();
try {
var sub = await APIClient.subscribeToChannelbyID(acc, widget.channelID);
widget.needsReload?.call();
await _initStateAsync(false);
if (sub.confirmed) {
Toaster.success("Success", 'Subscribed to channel');
} else {
Toaster.success("Success", 'Requested 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 _confirmForeignSubscription(Subscription sub) {
//TODO
void _unsubscribe() async {
final acc = AppAuth();
if (subscription == null) return;
try {
await APIClient.deleteSubscription(acc, widget.channelID, subscription!.subscriptionID);
widget.needsReload?.call();
await _initStateAsync(false);
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);
}
}
void _denyForeignSubscription(Subscription sub) {
//TODO
void _cancelForeignSubscription(Subscription sub) async {
final acc = AppAuth();
try {
await APIClient.unconfirmSubscription(acc, widget.channelID, subscription!.subscriptionID);
widget.needsReload?.call();
await _initStateAsync(false);
Toaster.success("Success", 'Subscription succesfully revoked');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to revoke subscription');
ApplicationLog.error('Failed to revoke subscription: ' + exc.toString(), trace: trace);
}
}
void _confirmForeignSubscription(Subscription sub) async {
final acc = AppAuth();
try {
await APIClient.confirmSubscription(acc, widget.channelID, subscription!.subscriptionID);
widget.needsReload?.call();
await _initStateAsync(false);
Toaster.success("Success", 'Subscription succesfully confirmed');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to confirm subscription');
ApplicationLog.error('Failed to confirm subscription: ' + exc.toString(), trace: trace);
}
}
void _denyForeignSubscription(Subscription sub) async {
final acc = AppAuth();
try {
await APIClient.deleteSubscription(acc, widget.channelID, subscription!.subscriptionID);
widget.needsReload?.call();
await _initStateAsync(false);
Toaster.success("Success", 'Subscription request succesfully denied');
} catch (exc, trace) {
Toaster.error("Error", 'Failed to deny subscription');
ApplicationLog.error('Failed to deny subscription: ' + exc.toString(), trace: trace);
}
}
String _formatSubscriptionStatus(Subscription? subscription) {

View File

@ -72,7 +72,7 @@ class UI {
splashColor: Theme.of(context).splashColor,
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: child,
),
),

View File

@ -411,7 +411,6 @@ func (h APIHandler) ListChannelMessages(pctx ginext.PreContext) ginext.HTTPRespo
type query struct {
PageSize *int `json:"page_size" form:"page_size"`
NextPageToken *string `json:"next_page_token" form:"next_page_token"`
Filter *string `json:"filter" form:"filter"`
Trimmed *bool `json:"trimmed" form:"trimmed"`
}
type response struct {