From ab4b40ab75814a0a92bab012a7d1ef789ff11870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Sun, 13 Apr 2025 19:47:18 +0200 Subject: [PATCH] implement keytoken list and all-messages list --- flutter/lib/api/api_client.dart | 22 +- flutter/lib/models/keytoken.dart | 17 +- flutter/lib/pages/account/account.dart | 12 +- .../lib/pages/channel_view/channel_view.dart | 2 + .../lib/pages/client_list/client_list.dart | 2 +- .../pages/client_list/client_list_item.dart | 4 - .../filtered_message_view.dart | 2 +- .../pages/keytoken_list/keytoken_list.dart | 86 ++++ .../keytoken_list/keytoken_list_item.dart | 113 +++++ .../pages/keytoken_view/keytoken_view.dart | 435 ++++++++++++++++++ .../lib/pages/message_view/message_view.dart | 4 +- 11 files changed, 685 insertions(+), 14 deletions(-) create mode 100644 flutter/lib/pages/keytoken_list/keytoken_list.dart create mode 100644 flutter/lib/pages/keytoken_list/keytoken_list_item.dart create mode 100644 flutter/lib/pages/keytoken_view/keytoken_view.dart diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index ac57d21..50ed8a5 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -39,6 +39,7 @@ class MessageFilter { DateTime? timeBefore; DateTime? timeAfter; bool? hasSenderName; + List? senderUserID; MessageFilter({ this.channelIDs, @@ -49,6 +50,7 @@ class MessageFilter { this.priority, this.timeBefore, this.timeAfter, + this.senderUserID, }); } @@ -281,7 +283,7 @@ class APIClient { ); } - static Future<(String, List)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, MessageFilter? filter}) async { + static Future<(String, List)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, MessageFilter? filter, bool? includeNonSuscribed}) async { return await _request( name: 'getMessageList', method: 'GET', @@ -298,6 +300,8 @@ class APIClient { if (filter?.timeAfter != null) 'after': [filter!.timeAfter!.toIso8601String()], if (filter?.priority != null) 'priority': filter!.priority!.map((p) => p.toString()).toList(), if (filter?.usedKeys != null) 'used_key': filter!.usedKeys!, + if (filter?.senderUserID != null) 'sender_user_id': filter!.senderUserID!, + if (includeNonSuscribed ?? false) 'subscription_status': ['all'], }, fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'), authToken: auth.getToken(), @@ -426,6 +430,22 @@ class APIClient { ); } + static Future updateKeyToken(TokenSource auth, String kid, {String? name, bool? allChannels, List? channels, String? permissions}) async { + return await _request( + name: 'updateKeyToken', + method: 'PATCH', + relURL: 'users/${auth.getUserID()}/keys/${kid}', + jsonBody: { + if (name != null) 'name': name, + if (allChannels != null) 'all_channels': allChannels, + if (channels != null) 'channels': channels, + if (permissions != null) 'permissions': permissions, + }, + fn: KeyToken.fromJson, + authToken: auth.getToken(), + ); + } + static Future createKeyToken(TokenSource auth, String name, String perm, bool allChannels, {List? channels}) async { return await _request( name: 'createKeyToken', diff --git a/flutter/lib/models/keytoken.dart b/flutter/lib/models/keytoken.dart index 3f13d51..d83beca 100644 --- a/flutter/lib/models/keytoken.dart +++ b/flutter/lib/models/keytoken.dart @@ -2,7 +2,7 @@ class KeyToken { final String keytokenID; final String name; final String timestampCreated; - final String? timestampLastused; + final String? timestampLastUsed; final String ownerUserID; final bool allChannels; final List channels; @@ -13,7 +13,7 @@ class KeyToken { required this.keytokenID, required this.name, required this.timestampCreated, - required this.timestampLastused, + required this.timestampLastUsed, required this.ownerUserID, required this.allChannels, required this.channels, @@ -26,7 +26,7 @@ class KeyToken { keytokenID: json['keytoken_id'] as String, name: json['name'] as String, timestampCreated: json['timestamp_created'] as String, - timestampLastused: json['timestamp_lastused'] as String?, + timestampLastUsed: json['timestamp_lastused'] as String?, ownerUserID: json['owner_user_id'] as String, allChannels: json['all_channels'] as bool, channels: (json['channels'] as List).map((e) => e as String).toList(), @@ -38,6 +38,17 @@ class KeyToken { static List fromJsonArray(List jsonArr) { return jsonArr.map((e) => KeyToken.fromJson(e as Map)).toList(); } + + KeyTokenPreview toPreview() { + return KeyTokenPreview( + keytokenID: keytokenID, + name: name, + ownerUserID: ownerUserID, + allChannels: allChannels, + channels: channels, + permissions: permissions, + ); + } } class KeyTokenWithToken { diff --git a/flutter/lib/pages/account/account.dart b/flutter/lib/pages/account/account.dart index 6aae648..8ebcbcf 100644 --- a/flutter/lib/pages/account/account.dart +++ b/flutter/lib/pages/account/account.dart @@ -9,6 +9,8 @@ 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/pages/client_list/client_list.dart'; +import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart'; +import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list.dart'; import 'package:simplecloudnotifier/pages/sender_list/sender_list.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; @@ -384,8 +386,10 @@ class _AccountRootPageState extends State { _buildNumberCard(context, 'Subscriptions', futureSubscriptionCount, () {/*TODO*/}), _buildNumberCard(context, 'Clients', futureClientCount, () { Navi.push(context, () => ClientListPage()); - }), - _buildNumberCard(context, 'Keys', futureKeyCount, () {/*TODO*/}), + }), + _buildNumberCard(context, 'Keys', futureKeyCount, () { + Navi.push(context, () => KeyTokenListPage()); + }), _buildNumberCard(context, 'Channels', futureChannelSubscribedCount, () { Navi.push(context, () => ChannelListExtendedPage()); }), @@ -402,7 +406,9 @@ class _AccountRootPageState extends State { Text('Messages', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)), ], ), - onTap: () {/*TODO*/}, + onTap: () { + Navi.push(context, () => FilteredMessageViewPage(title: "All Messages", filter: MessageFilter(senderUserID: [user.userID]))); + }, ), ]; } diff --git a/flutter/lib/pages/channel_view/channel_view.dart b/flutter/lib/pages/channel_view/channel_view.dart index 635bd68..7ecd518 100644 --- a/flutter/lib/pages/channel_view/channel_view.dart +++ b/flutter/lib/pages/channel_view/channel_view.dart @@ -166,6 +166,7 @@ class _ChannelViewPageState extends State { } Widget _buildOwnedChannelView(BuildContext context, Channel channel) { + final userAccUserID = context.select((v) => v.userID); final isSubscribed = (subscription != null && subscription!.confirmed); return SingleChildScrollView( @@ -208,6 +209,7 @@ class _ChannelViewPageState extends State { Navi.push(context, () => ChannelMessageViewPage(channel: channel)); }, ), + if (channel.ownerUserID == userAccUserID) UI.button(text: "Delete Channel", onPressed: () {/*TODO*/}, color: Colors.red[900]), ], ), ), diff --git a/flutter/lib/pages/client_list/client_list.dart b/flutter/lib/pages/client_list/client_list.dart index 3ee12e4..fc84dc5 100644 --- a/flutter/lib/pages/client_list/client_list.dart +++ b/flutter/lib/pages/client_list/client_list.dart @@ -64,7 +64,7 @@ class _ClientListPageState extends State { @override Widget build(BuildContext context) { return SCNScaffold( - title: "Client", + title: "Clients", showSearch: false, showShare: false, child: Padding( diff --git a/flutter/lib/pages/client_list/client_list_item.dart b/flutter/lib/pages/client_list/client_list_item.dart index 25a7666..ea8f2f4 100644 --- a/flutter/lib/pages/client_list/client_list_item.dart +++ b/flutter/lib/pages/client_list/client_list_item.dart @@ -1,11 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; -import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/client.dart'; -import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart'; -import 'package:simplecloudnotifier/state/globals.dart'; -import 'package:simplecloudnotifier/utils/navi.dart'; enum ClientListItemMode { Messages, diff --git a/flutter/lib/pages/filtered_message_view/filtered_message_view.dart b/flutter/lib/pages/filtered_message_view/filtered_message_view.dart index 5efeda4..66fe985 100644 --- a/flutter/lib/pages/filtered_message_view/filtered_message_view.dart +++ b/flutter/lib/pages/filtered_message_view/filtered_message_view.dart @@ -70,7 +70,7 @@ class _FilteredMessageViewPageState extends State { }); } - final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize, filter: this.widget.filter); + final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize, filter: this.widget.filter, includeNonSuscribed: true); SCNDataCache().addToMessageCache(newItems); // no await diff --git a/flutter/lib/pages/keytoken_list/keytoken_list.dart b/flutter/lib/pages/keytoken_list/keytoken_list.dart new file mode 100644 index 0000000..dbee27c --- /dev/null +++ b/flutter/lib/pages/keytoken_list/keytoken_list.dart @@ -0,0 +1,86 @@ +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/keytoken.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; +import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list_item.dart'; + +class KeyTokenListPage extends StatefulWidget { + const KeyTokenListPage({super.key}); + + @override + State createState() => _KeyTokenListPageState(); +} + +class _KeyTokenListPageState extends State { + final PagingController _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0); + + @override + void initState() { + super.initState(); + + _pagingController.addPageRequestListener(_fetchPage); + + _pagingController.refresh(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + + @override + void dispose() { + ApplicationLog.debug('KeyTokenListPage::dispose'); + _pagingController.dispose(); + super.dispose(); + } + + Future _fetchPage(int pageKey) async { + final acc = Provider.of(context, listen: false); + + ApplicationLog.debug('Start KeyTokenListPage::_pagingController::_fetchPage [ ${pageKey} ]'); + + if (!acc.isAuth()) { + _pagingController.error = 'Not logged in'; + return; + } + + try { + final items = (await APIClient.getKeyTokenList(acc)).toList(); + + items.sort((a, b) => -1 * a.timestampCreated.compareTo(b.timestampCreated)); + + _pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null); + } catch (exc, trace) { + _pagingController.error = exc.toString(); + ApplicationLog.error('Failed to list keytokens: ' + exc.toString(), trace: trace); + } + } + + @override + Widget build(BuildContext context) { + return SCNScaffold( + title: "Keys", + showSearch: false, + showShare: false, + child: Padding( + padding: EdgeInsets.fromLTRB(8, 4, 8, 4), + child: RefreshIndicator( + onRefresh: () => Future.sync( + () => _pagingController.refresh(), + ), + child: PagedListView( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => KeyTokenListItem(item: item), + ), + ), + ), + ), + ); + } +} diff --git a/flutter/lib/pages/keytoken_list/keytoken_list_item.dart b/flutter/lib/pages/keytoken_list/keytoken_list_item.dart new file mode 100644 index 0000000..86735dc --- /dev/null +++ b/flutter/lib/pages/keytoken_list/keytoken_list_item.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:intl/intl.dart'; +import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/models/keytoken.dart'; +import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart'; +import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_view.dart'; +import 'package:simplecloudnotifier/utils/navi.dart'; + +enum KeyTokenListItemMode { + Messages, + Extended, +} + +class KeyTokenListItem extends StatelessWidget { + static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting + + const KeyTokenListItem({ + required this.item, + super.key, + }); + + final KeyToken item; + + @override + Widget build(BuildContext context) { + 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: () { + Navi.push(context, () => KeyTokenViewPage(keytokenID: item.keytokenID, preloadedData: item, needsReload: null)); + }, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Icon(FontAwesomeIcons.solidGearCode, color: Theme.of(context).colorScheme.outline, size: 32), + SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: Text( + item.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + Text( + (item.timestampLastUsed == null) ? '' : KeyTokenListItem._dateFormat.format(DateTime.parse(item.timestampLastUsed!).toLocal()), + style: const TextStyle(fontSize: 14), + ), + ], + ), + SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Text( + "Permissions: " + _formatPermissions(item.permissions, item.allChannels, item.channels), + style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)), + ), + ), + Text(item.messagesSent.toString(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), + ], + ), + ], + ), + ), + SizedBox(width: 4), + GestureDetector( + onTap: () { + Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(usedKeys: [item.keytokenID]))); + }, + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon(FontAwesomeIcons.solidEnvelopes, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128), size: 24), + ), + ), + ], + ), + ), + ), + ); + } + + String _formatPermissions(String v, bool allChannels, List channels) { + var splt = v.split(';'); + + if (splt.length == 0) return "None"; + + var a = splt.contains("A"); + var ur = splt.contains("UR"); + var cr = splt.contains("CR"); + var cs = splt.contains("CS"); + + if (a) return "Admin"; + if (cr && cs && allChannels) return "Read+Send"; + if (cr && cs && !allChannels) return "Read+Send (${channels.length} channel${channels.length == 1 ? '' : 's'})"; + if (ur && !cr && !cs) return "Account-Read"; + if (cr && !cs && !allChannels) return "Read-only (${channels.length} channel${channels.length == 1 ? '' : 's'})"; + if (cr && !cs && allChannels) return "Read-only"; + if (cs && !allChannels) return "Send-Only (${channels.length} channel${channels.length == 1 ? '' : 's'})"; + if (cs && allChannels) return "Send-Only"; + + return "{ " + v + " | " + (allChannels ? 'all' : '${channels.length}') + " }"; + } +} diff --git a/flutter/lib/pages/keytoken_view/keytoken_view.dart b/flutter/lib/pages/keytoken_view/keytoken_view.dart new file mode 100644 index 0000000..e37a6c4 --- /dev/null +++ b/flutter/lib/pages/keytoken_view/keytoken_view.dart @@ -0,0 +1,435 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:intl/intl.dart'; +import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/components/layout/scaffold.dart'; +import 'package:simplecloudnotifier/models/keytoken.dart'; +import 'package:simplecloudnotifier/models/user.dart'; +import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; +import 'package:simplecloudnotifier/state/app_bar_state.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/types/immediate_future.dart'; +import 'package:simplecloudnotifier/utils/navi.dart'; +import 'package:simplecloudnotifier/utils/toaster.dart'; +import 'package:simplecloudnotifier/utils/ui.dart'; +import 'package:provider/provider.dart'; + +class KeyTokenViewPage extends StatefulWidget { + const KeyTokenViewPage({ + required this.keytokenID, + required this.preloadedData, + required this.needsReload, + super.key, + }); + + final String keytokenID; + final KeyToken? preloadedData; + + final void Function()? needsReload; + + @override + State createState() => _KeyTokenViewPageState(); +} + +enum EditState { none, editing, saving } + +enum KeyTokenViewPageInitState { loading, okay, error } + +class _KeyTokenViewPageState extends State { + static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting + + late ImmediateFuture _futureOwner; + + final TextEditingController _ctrlName = TextEditingController(); + + int _loadingIndeterminateCounter = 0; + + EditState _editName = EditState.none; + String? _nameOverride = null; + + KeyTokenPreview? keytokenPreview; + KeyToken? keytoken; + + KeyTokenViewPageInitState loadingState = KeyTokenViewPageInitState.loading; + String errorMessage = ''; + + @override + void initState() { + _initStateAsync(true); + + super.initState(); + } + + Future _initStateAsync(bool usePreload) async { + final userAcc = Provider.of(context, listen: false); + + if (widget.preloadedData != null && usePreload) { + keytoken = widget.preloadedData!; + keytokenPreview = widget.preloadedData!.toPreview(); + } else { + try { + var p = await APIClient.getKeyTokenPreviewByID(userAcc, widget.keytokenID); + setState(() { + keytokenPreview = p; + }); + + if (p.ownerUserID == userAcc.userID) { + var r = await APIClient.getKeyToken(userAcc, widget.keytokenID); + setState(() { + keytoken = r; + }); + } else { + setState(() { + keytoken = null; + }); + } + } catch (exc, trace) { + ApplicationLog.error('Failed to load data: ' + exc.toString(), trace: trace); + Toaster.error("Error", 'Failed to load data'); + this.errorMessage = 'Failed to load data: ' + exc.toString(); + this.loadingState = KeyTokenViewPageInitState.error; + return; + } + } + + setState(() { + this.loadingState = KeyTokenViewPageInitState.okay; + + assert(keytokenPreview != null); + + if (this.keytokenPreview!.ownerUserID == userAcc.userID) { + var cacheUser = userAcc.getUserOrNull(); + if (cacheUser != null) { + _futureOwner = ImmediateFuture.ofValue(cacheUser.toPreview()); + } else { + _futureOwner = ImmediateFuture.ofFuture(_getOwner(userAcc)); + } + } else { + _futureOwner = ImmediateFuture.ofFuture(APIClient.getUserPreview(userAcc, this.keytokenPreview!.ownerUserID)); + } + }); + } + + @override + void dispose() { + _ctrlName.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final userAcc = Provider.of(context, listen: false); + + var title = "Key"; + + Widget child; + + if (loadingState == KeyTokenViewPageInitState.loading) { + child = Center(child: CircularProgressIndicator()); + } else if (loadingState == KeyTokenViewPageInitState.error) { + child = Center(child: Text('Error: ' + errorMessage)); //TODO better error + } else if (loadingState == KeyTokenViewPageInitState.okay && keytokenPreview!.ownerUserID == userAcc.userID) { + child = _buildOwnedKeyTokenView(context, this.keytoken!); + title = this.keytoken!.name; + } else { + child = _buildForeignKeyTokenView(context, this.keytokenPreview!); + title = keytokenPreview!.name; + } + + return SCNScaffold( + title: title, + showSearch: false, + showShare: false, + child: child, + ); + } + + Widget _buildOwnedKeyTokenView(BuildContext context, KeyToken keytoken) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 8), + UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidIdCardClip, + title: 'KeyTokenID', + values: [keytoken.keytokenID], + ), + _buildNameCard(context, true), + UI.metaCard( + context: context, + icon: FontAwesomeIcons.clock, + title: 'Created', + values: [_KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampCreated).toLocal())], + ), + UI.metaCard( + context: context, + icon: FontAwesomeIcons.clockTwo, + title: 'Last Used', + values: [(keytoken.timestampLastUsed == null) ? 'Never' : _KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampLastUsed!).toLocal())], + ), + _buildOwnerCard(context, true), + UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidEnvelope, + title: 'Messages', + values: [keytoken.messagesSent.toString()], + mainAction: () { + Navi.push(context, () => FilteredMessageViewPage(title: keytoken.name, filter: MessageFilter(usedKeys: [keytoken.keytokenID]))); + }, + ), + ..._buildPermissionCard(context, true, keytoken.toPreview()), + UI.button(text: "Delete Key", onPressed: _deleteKey, color: Colors.red[900]), + ], + ), + ), + ); + } + + Widget _buildForeignKeyTokenView(BuildContext context, KeyTokenPreview keytoken) { + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 8), + UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidIdCardClip, + title: 'KeyTokenID', + values: [keytoken.keytokenID], + ), + _buildNameCard(context, false), + _buildOwnerCard(context, false), + ..._buildPermissionCard(context, false, keytoken), + ], + ), + ), + ); + } + + Widget _buildOwnerCard(BuildContext context, bool isOwned) { + return FutureBuilder( + future: _futureOwner.future, + builder: (context, snapshot) { + if (snapshot.hasData) { + return UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidUser, + title: 'Owner', + values: [keytokenPreview!.ownerUserID + (isOwned ? ' (you)' : ''), if (snapshot.data?.username != null) snapshot.data!.username!], + ); + } else { + return UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidUser, + title: 'Owner', + values: [keytokenPreview!.ownerUserID + (isOwned ? ' (you)' : '')], + ); + } + }, + ); + } + + Widget _buildNameCard(BuildContext context, bool isOwned) { + if (_editName == EditState.editing) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0), + child: UI.box( + context: context, + padding: EdgeInsets.fromLTRB(16, 2, 4, 2), + child: Row( + children: [ + Container(child: Center(child: FaIcon(FontAwesomeIcons.solidInputText, size: 18)), height: 43), + SizedBox(width: 16), + Expanded( + child: TextField( + autofocus: true, + controller: _ctrlName, + decoration: new InputDecoration.collapsed(hintText: 'Name'), + ), + ), + SizedBox(width: 12), + SizedBox(width: 4), + IconButton(icon: FaIcon(FontAwesomeIcons.solidFloppyDisk), onPressed: _saveName), + ], + ), + ), + ); + } else if (_editName == EditState.none) { + return UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidInputText, + title: 'Name', + values: [_nameOverride ?? keytokenPreview!.name], + iconActions: isOwned ? [(FontAwesomeIcons.penToSquare, _showEditName)] : [], + ); + } else if (_editName == EditState.saving) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0), + child: UI.box( + context: context, + padding: EdgeInsets.fromLTRB(16, 2, 4, 2), + child: Row( + children: [ + Container(child: Center(child: FaIcon(FontAwesomeIcons.solidInputText, size: 18)), height: 43), + SizedBox(width: 16), + Expanded(child: SizedBox()), + SizedBox(width: 12), + SizedBox(width: 4), + Padding(padding: const EdgeInsets.all(8.0), child: SizedBox(width: 18, height: 18, child: CircularProgressIndicator())), + ], + ), + ), + ); + } else { + throw 'Invalid EditDisplayNameState: $_editName'; + } + } + + void _showEditName() { + setState(() { + _ctrlName.text = _nameOverride ?? keytokenPreview?.name ?? ''; + _editName = EditState.editing; + if (_editName == EditState.editing) _editName = EditState.none; + }); + } + + void _saveName() async { + final userAcc = Provider.of(context, listen: false); + + final newName = _ctrlName.text; + + try { + setState(() { + _editName = EditState.saving; + }); + + final newKeyToken = await APIClient.updateKeyToken(userAcc, widget.keytokenID, name: newName); + + setState(() { + _editName = EditState.none; + _nameOverride = newKeyToken.name; + }); + + widget.needsReload?.call(); + } catch (exc, trace) { + ApplicationLog.error('Failed to save DisplayName: ' + exc.toString(), trace: trace); + Toaster.error("Error", 'Failed to save DisplayName'); + } + } + + Future _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, keytokenPreview!.ownerUserID); + + //await Future.delayed(const Duration(seconds: 10), () {}); + + return owner; + } finally { + _incLoadingIndeterminateCounter(-1); + } + } + + void _incLoadingIndeterminateCounter(int delta) { + setState(() { + _loadingIndeterminateCounter += delta; + AppBarState().setLoadingIndeterminate(_loadingIndeterminateCounter > 0); + }); + } + + List _buildPermissionCard(BuildContext context, bool isOwned, KeyTokenPreview keyToken) { + Widget w1; + Widget w2; + + if (isOwned) { + w1 = UI.metaCard( + context: context, + icon: FontAwesomeIcons.shieldKeyhole, + title: 'Permissions', + values: _formatPermissions(keyToken.permissions), + iconActions: [(FontAwesomeIcons.penToSquare, _editPermissions)], + ); + } else { + w1 = UI.metaCard( + context: context, + icon: FontAwesomeIcons.shieldKeyhole, + title: 'Permissions', + values: _formatPermissions(keyToken.permissions), + ); + } + + if (isOwned) { + w2 = UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidSnake, + title: 'Channels', + values: (keyToken.allChannels) ? ['All Channels'] : keyToken.channels, + iconActions: [(FontAwesomeIcons.penToSquare, _editChannels)], + ); + } else { + w2 = UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidSnake, + title: 'Channels', + values: (keyToken.allChannels) ? ['All Channels'] : keyToken.channels, + ); + } + + return [w1, w2]; + } + + List _formatPermissions(String v) { + var splt = v.split(';'); + + if (splt.length == 0) return ["None"]; + + List result = []; + + if (splt.contains("A")) result.add("Admin"); + if (splt.contains("UR")) result.add("Read Account"); + if (splt.contains("CR")) result.add("Read Messages"); + if (splt.contains("CS")) result.add("Send Messages"); + + return result; + } + + void _editPermissions() { + final acc = Provider.of(context, listen: false); + + //TODO prevent editing current admin/read token + + //TODO + + Toaster.info("Not implemented", "Currently not implemented"); + } + + void _editChannels() { + final acc = Provider.of(context, listen: false); + + //TODO prevent editing current admin/read token + + //TODO + + Toaster.info("Not implemented", "Currently not implemented"); + } + + void _deleteKey() { + final acc = Provider.of(context, listen: false); + + //TODO prevent deleting current admin/read token + + //TODO + + Toaster.info("Not implemented", "Currently not implemented"); + } +} diff --git a/flutter/lib/pages/message_view/message_view.dart b/flutter/lib/pages/message_view/message_view.dart index f34624e..17a5932 100644 --- a/flutter/lib/pages/message_view/message_view.dart +++ b/flutter/lib/pages/message_view/message_view.dart @@ -162,7 +162,9 @@ class _MessageViewPageState extends State { icon: FontAwesomeIcons.solidGearCode, title: 'KeyToken', values: [message.usedKeyID, token?.name ?? '...'], - mainAction: () => {/*TODO*/}, + mainAction: () => { + Navi.push(context, () => FilteredMessageViewPage(title: token?.name ?? message.usedKeyID, filter: MessageFilter(usedKeys: [message.usedKeyID]))) + }, ), UI.metaCard( context: context,