channel_view page

This commit is contained in:
Mike Schwörer 2024-06-25 20:49:40 +02:00
parent e2dbe8866d
commit 2b23404461
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
7 changed files with 367 additions and 177 deletions

View File

@ -247,6 +247,16 @@ class APIClient {
); );
} }
static Future<List<Subscription>> getChannelSubscriptions(TokenSource auth, String cid) async {
return await _request(
name: 'getChannelSubscriptions',
method: 'GET',
relURL: 'users/${auth.getUserID()}/channels/${cid}/subscriptions',
fn: (json) => Subscription.fromJsonArray(json['subscriptions'] as List<dynamic>),
authToken: auth.getToken(),
);
}
static Future<List<Client>> getClientList(TokenSource auth) async { static Future<List<Client>> getClientList(TokenSource auth) async {
return await _request( return await _request(
name: 'getClientList', name: 'getClientList',

View File

@ -86,6 +86,10 @@ class User {
'max_user_message_id_length': maxUserMessageIDLength, 'max_user_message_id_length': maxUserMessageIDLength,
}; };
} }
UserPreview toPreview() {
return UserPreview(userID: userID, username: username);
}
} }
class UserWithClientsAndKeys { class UserWithClientsAndKeys {

View File

@ -110,8 +110,10 @@ class _ChannelListItemState extends State<ChannelListItem> {
Widget _buildIcon(BuildContext context) { Widget _buildIcon(BuildContext context) {
if (widget.subscription == null) { if (widget.subscription == null) {
return Icon(FontAwesomeIcons.solidSquareDashed, color: Theme.of(context).colorScheme.outline, size: 32); // not-subscribed return Icon(FontAwesomeIcons.solidSquareDashed, color: Theme.of(context).colorScheme.outline, size: 32); // not-subscribed
} 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)
} else if (widget.subscription!.confirmed) { } else if (widget.subscription!.confirmed) {
return Icon(FontAwesomeIcons.solidSquareRss, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed return Icon(FontAwesomeIcons.solidSquareShareNodes, color: Theme.of(context).colorScheme.onPrimaryContainer, size: 32); // subscribed (foreign channel)
} else { } else {
return Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32); // requested return Icon(FontAwesomeIcons.solidSquareEnvelope, color: Theme.of(context).colorScheme.tertiary, size: 32); // requested
} }

View File

@ -1,21 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/models/subscription.dart'; import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/types/immediate_future.dart';
import 'package:simplecloudnotifier/utils/ui.dart'; import 'package:simplecloudnotifier/utils/ui.dart';
import 'package:provider/provider.dart';
class ChannelViewPage extends StatefulWidget { class ChannelViewPage extends StatefulWidget {
const ChannelViewPage({ const ChannelViewPage({
@ -32,10 +28,39 @@ class ChannelViewPage extends StatefulWidget {
} }
class _ChannelViewPageState extends State<ChannelViewPage> { class _ChannelViewPageState extends State<ChannelViewPage> {
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm'); late ImmediateFuture<String?> _futureSubscribeKey;
late ImmediateFuture<List<Subscription>> _futureSubscriptions;
late ImmediateFuture<UserPreview> _futureOwner;
int _loadingIndeterminateCounter = 0;
@override @override
void initState() { void initState() {
final userAcc = Provider.of<AppAuth>(context, listen: false);
if (widget.channel.ownerUserID == userAcc.userID) {
if (widget.channel.subscribeKey != null) {
_futureSubscribeKey = ImmediateFuture<String?>.ofValue(widget.channel.subscribeKey);
} else {
_futureSubscribeKey = ImmediateFuture<String?>.ofFuture(_getSubScribeKey(userAcc));
}
_futureSubscriptions = ImmediateFuture<List<Subscription>>.ofFuture(_listSubscriptions(userAcc));
} else {
_futureSubscribeKey = ImmediateFuture<String?>.ofValue(null);
_futureSubscriptions = ImmediateFuture<List<Subscription>>.ofValue([]);
}
if (widget.channel.ownerUserID == userAcc.userID) {
var cacheUser = userAcc.getUserOrNull();
if (cacheUser != null) {
_futureOwner = ImmediateFuture<UserPreview>.ofValue(cacheUser.toPreview());
} else {
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(_getOwner(userAcc));
}
} else {
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, widget.channel.ownerUserID));
}
super.initState(); super.initState();
} }
@ -57,93 +82,115 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
Widget _buildChannelView(BuildContext context) { Widget _buildChannelView(BuildContext context) {
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID); final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
final isOwned = (widget.channel.ownerUserID == userAccUserID);
final isSubscribed = (widget.subscription != null && widget.subscription!.confirmed);
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
..._buildChannelHeader(context),
SizedBox(height: 8),
_buildQRCode(context), _buildQRCode(context),
SizedBox(height: 8), SizedBox(height: 8),
//TODO icons UI.metaCard(
_buildMetaCard(context, FontAwesomeIcons.solidQuestion, 'ChannelID', ['...'], null),
_buildMetaCard(context, FontAwesomeIcons.solidQuestion, 'InternalName', ['...'], null),
_buildMetaCard(context, FontAwesomeIcons.solidQuestion, 'DisplayName', ['...'], null), //TODO edit icon on right to edit name
_buildMetaCard(context, FontAwesomeIcons.solidQuestion, 'Subscription (own)', ['...'], null), //TODO sub/unsub icon on right
//TODO list foreign subscriptions (with accept/decline/delete button on right)
_buildMetaCard(context, FontAwesomeIcons.solidQuestion, 'Messages', ['...'], () {/*TODO*/}),
],
),
),
);
}
List<Widget> _buildChannelHeader(BuildContext context) {
return [
Text(widget.channel.displayName, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
];
}
Widget _buildMetaCard(BuildContext context, IconData icn, String title, List<String> values, void Function()? action) {
final container = UI.box(
context: context, context: context,
padding: EdgeInsets.fromLTRB(16, 2, 4, 2), icon: FontAwesomeIcons.solidIdCardClip,
child: Row( title: 'ChannelID',
children: [ values: [widget.channel.channelID],
FaIcon(icn, size: 18), ),
SizedBox(width: 16), UI.metaCard(
Column( context: context,
crossAxisAlignment: CrossAxisAlignment.start, icon: FontAwesomeIcons.solidInputNumeric,
children: [ title: 'InternalName',
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), values: [widget.channel.internalName],
for (final val in values) Text(val, style: const TextStyle(fontSize: 14)), ),
], UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidInputText,
title: 'DisplayName',
values: [widget.channel.displayName],
iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, _rename)] : [],
),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidDiagramSubtask,
title: 'Subscription (own)',
values: [_formatSubscriptionStatus(widget.subscription)],
iconActions: isSubscribed ? [(FontAwesomeIcons.solidSquareXmark, _unsubscribe)] : [(FontAwesomeIcons.solidSquareRss, _subscribe)],
),
_buildForeignSubscriptions(context),
_buildOwnerCard(context, isOwned),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidEnvelope,
title: 'Messages',
values: [widget.channel.messagesSent.toString()],
mainAction: () {/*TODO*/},
), ),
], ],
), ),
),
); );
}
if (action == null) { Widget _buildForeignSubscriptions(BuildContext context) {
return Padding( return FutureBuilder(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0), future: _futureSubscriptions.future,
child: container, builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (final sub in snapshot.data!.where((sub) => sub.subscriptionID != widget.subscription?.subscriptionID))
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidDiagramSuccessor,
title: 'Subscription (other)',
values: [_formatSubscriptionStatus(sub)],
iconActions: _getForignSubActions(sub),
),
],
); );
} else { } else {
return Padding( return SizedBox();
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0), }
child: InkWell( },
splashColor: Theme.of(context).splashColor,
onTap: action,
child: container,
),
); );
} }
}
String _preformatTitle(SCNMessage message) { Widget _buildOwnerCard(BuildContext context, bool isOwned) {
return message.title.replaceAll('\n', '').replaceAll('\r', '').replaceAll('\t', ' '); return FutureBuilder(
} future: _futureOwner.future,
builder: (context, snapshot) {
String _prettyPrintPriority(int priority) { if (snapshot.hasData) {
switch (priority) { return UI.metaCard(
case 0: context: context,
return 'Low (0)'; icon: FontAwesomeIcons.solidUser,
case 1: title: 'Owner',
return 'Normal (1)'; values: [widget.channel.ownerUserID + (isOwned ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!],
case 2: );
return 'High (2)'; } else {
default: return UI.metaCard(
return 'Unknown ($priority)'; context: context,
icon: FontAwesomeIcons.solidUser,
title: 'Owner',
values: [widget.channel.ownerUserID + (isOwned ? ' (you)' : '')],
);
} }
},
);
} }
Widget _buildQRCode(BuildContext context) { Widget _buildQRCode(BuildContext context) {
var text = 'TODO' + widget.channel.channelID; //TODO subkey+channelid with deeplink-y return FutureBuilder(
future: _futureSubscribeKey.future,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
var text = 'TODO' + '\n' + widget.channel.channelID + '\n' + snapshot.data!; //TODO deeplink-y (also perhaps just bas64 everything together?)
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
//TODO share Share.share(text, subject: widget.channel.displayName);
}, },
child: Center( child: Center(
child: QrImageView( child: QrImageView(
@ -161,5 +208,120 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
), ),
), ),
); );
} else if (snapshot.hasData && snapshot.data == null) {
return const SizedBox(
width: 300.0,
height: 300.0,
child: Center(child: Icon(FontAwesomeIcons.solidSnake, size: 64)),
);
} else {
return const SizedBox(
width: 300.0,
height: 300.0,
child: Center(child: CircularProgressIndicator()),
);
}
},
);
}
void _rename() {
//TODO
}
void _subscribe() {
//TODO
}
void _unsubscribe() {
//TODO
}
void _cancelForeignSubscription(Subscription sub) {
//TODO
}
void _confirmForeignSubscription(Subscription sub) {
//TODO
}
void _denyForeignSubscription(Subscription sub) {
//TODO
}
String _formatSubscriptionStatus(Subscription? subscription) {
if (subscription == null) {
return 'Not Subscribed';
} else if (subscription.confirmed) {
return 'Subscribed';
} else {
return 'Requested';
}
}
Future<String?> _getSubScribeKey(AppAuth auth) async {
try {
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
_incLoadingIndeterminateCounter(1);
var channel = await APIClient.getChannel(auth, widget.channel.channelID);
//await Future.delayed(const Duration(seconds: 10), () {});
return channel.channel.subscribeKey;
} finally {
_incLoadingIndeterminateCounter(-1);
}
}
Future<List<Subscription>> _listSubscriptions(AppAuth auth) async {
try {
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
_incLoadingIndeterminateCounter(1);
var subs = await APIClient.getChannelSubscriptions(auth, widget.channel.channelID);
//await Future.delayed(const Duration(seconds: 10), () {});
return subs;
} finally {
_incLoadingIndeterminateCounter(-1);
}
}
Future<UserPreview> _getOwner(AppAuth auth) async {
try {
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
_incLoadingIndeterminateCounter(1);
final owner = APIClient.getUserPreview(auth, widget.channel.ownerUserID);
//await Future.delayed(const Duration(seconds: 10), () {});
return owner;
} finally {
_incLoadingIndeterminateCounter(-1);
}
}
List<(IconData, void Function())> _getForignSubActions(Subscription sub) {
if (sub.confirmed) {
return [(FontAwesomeIcons.solidSquareXmark, () => _cancelForeignSubscription(sub))];
} else {
return [
(FontAwesomeIcons.solidSquareCheck, () => _confirmForeignSubscription(sub)),
(FontAwesomeIcons.solidSquareXmark, () => _denyForeignSubscription(sub)),
];
}
}
void _incLoadingIndeterminateCounter(int delta) {
setState(() {
_loadingIndeterminateCounter += delta;
AppBarState().setLoadingIndeterminate(_loadingIndeterminateCounter > 0);
});
} }
} }

View File

@ -131,59 +131,54 @@ class _MessageViewPageState extends State<MessageViewPage> {
SizedBox(height: 8), SizedBox(height: 8),
if (message.content != null) ..._buildMessageContent(context, message), if (message.content != null) ..._buildMessageContent(context, message),
SizedBox(height: 8), SizedBox(height: 8),
if (message.senderName != null) _buildMetaCard(context, FontAwesomeIcons.solidSignature, 'Sender', [message.senderName!], () => {/*TODO*/}), if (message.senderName != null)
_buildMetaCard( UI.metaCard(
context, context: context,
FontAwesomeIcons.solidGearCode, icon: FontAwesomeIcons.solidSignature,
'KeyToken', title: 'Sender',
[ values: [message.senderName!],
message.usedKeyID, mainAction: () => {/*TODO*/},
token?.name ?? '...', ),
], UI.metaCard(
() => {/*TODO*/}), context: context,
_buildMetaCard( icon: FontAwesomeIcons.solidGearCode,
context, title: 'KeyToken',
FontAwesomeIcons.solidIdCardClip, values: [message.usedKeyID, token?.name ?? '...'],
'MessageID', mainAction: () => {/*TODO*/},
[ ),
message.messageID, UI.metaCard(
message.userMessageID ?? '', context: context,
], icon: FontAwesomeIcons.solidIdCardClip,
null), title: 'MessageID',
_buildMetaCard( values: [message.messageID, message.userMessageID ?? ''],
context, ),
FontAwesomeIcons.solidSnake, UI.metaCard(
'Channel', context: context,
[ icon: FontAwesomeIcons.solidSnake,
message.channelID, title: 'Channel',
channel?.displayName ?? message.channelInternalName, values: [message.channelID, channel?.displayName ?? message.channelInternalName],
], mainAction: () => {/*TODO*/},
() => {/*TODO*/}), ),
_buildMetaCard( UI.metaCard(
context, context: context,
FontAwesomeIcons.solidTimer, icon: FontAwesomeIcons.solidTimer,
'Timestamp', title: 'Timestamp',
[ values: [message.timestamp],
message.timestamp, ),
], UI.metaCard(
null), context: context,
_buildMetaCard( icon: FontAwesomeIcons.solidUser,
context, title: 'User',
FontAwesomeIcons.solidUser, values: [user?.userID ?? '...', user?.username ?? ''],
'User', mainAction: () => {/*TODO*/},
[ ),
user?.userID ?? '...', UI.metaCard(
user?.username ?? '', context: context,
], icon: FontAwesomeIcons.solidBolt,
() => {/*TODO*/}), //TODO title: 'Priority',
_buildMetaCard( values: [_prettyPrintPriority(message.priority)],
context, mainAction: () => {/*TODO*/},
FontAwesomeIcons.solidBolt, ),
'Priority',
[
_prettyPrintPriority(message.priority),
],
() => {/*TODO*/}), //TODO
if (message.senderUserID == userAccUserID) UI.button(text: "Delete Message", onPressed: () {/*TODO*/}, color: Colors.red[900]), if (message.senderUserID == userAccUserID) UI.button(text: "Delete Message", onPressed: () {/*TODO*/}, color: Colors.red[900]),
], ],
), ),
@ -260,42 +255,6 @@ class _MessageViewPageState extends State<MessageViewPage> {
]; ];
} }
Widget _buildMetaCard(BuildContext context, IconData icn, String title, List<String> values, void Function()? action) {
final container = UI.box(
context: context,
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
child: Row(
children: [
FaIcon(icn, size: 18),
SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
for (final val in values) Text(val, style: const TextStyle(fontSize: 14)),
],
),
],
),
);
if (action == null) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
child: container,
);
} else {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
child: InkWell(
splashColor: Theme.of(context).splashColor,
onTap: action,
child: container,
),
);
}
}
String _preformatTitle(SCNMessage message) { String _preformatTitle(SCNMessage message) {
return message.title.replaceAll('\n', '').replaceAll('\r', '').replaceAll('\t', ' '); return message.title.replaceAll('\n', '').replaceAll('\r', '').replaceAll('\t', ' ');
} }

View File

@ -182,6 +182,10 @@ class AppAuth extends ChangeNotifier implements TokenSource {
return user; return user;
} }
User? getUserOrNull() {
return _user?.$1;
}
Future<Client?> loadClient({bool force = false, Duration? forceIfOlder = null}) async { Future<Client?> loadClient({bool force = false, Duration? forceIfOlder = null}) async {
if (forceIfOlder != null && _client != null && _client!.$2.difference(DateTime.now()) > forceIfOlder) { if (forceIfOlder != null && _client != null && _client!.$2.difference(DateTime.now()) > forceIfOlder) {
force = true; force = true;
@ -212,6 +216,10 @@ class AppAuth extends ChangeNotifier implements TokenSource {
} }
} }
Client? getClientOrNull() {
return _client?.$1;
}
@override @override
String getToken() { String getToken() {
return _tokenAdmin!; return _tokenAdmin!;

View File

@ -106,4 +106,49 @@ class UI {
child: child, child: child,
); );
} }
static Widget metaCard({required BuildContext context, required IconData icon, required String title, required List<String> values, void Function()? mainAction, List<(IconData, void Function())>? iconActions}) {
final container = UI.box(
context: context,
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
child: Row(
children: [
FaIcon(icon, size: 18),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
for (final val in values) Text(val, style: const TextStyle(fontSize: 14)),
],
),
),
if (iconActions != null) ...[
SizedBox(width: 12),
for (final iconAction in iconActions) ...[
SizedBox(width: 4),
IconButton(icon: FaIcon(iconAction.$1), onPressed: iconAction.$2),
],
],
],
),
);
if (mainAction == null) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
child: container,
);
} else {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0),
child: InkWell(
splashColor: Theme.of(context).splashColor,
onTap: mainAction,
child: container,
),
);
}
}
} }