From 95d51c82e9262ca4472c7d2829766cc220947a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Fri, 7 Jun 2024 23:44:32 +0200 Subject: [PATCH] a bit of work on the message page --- flutter/_utils/autoreload.sh | 2 +- flutter/lib/api/api_client.dart | 10 + flutter/lib/api/api_exception.dart | 2 +- flutter/lib/models/api_error.dart | 60 +++++- .../lib/pages/message_view/message_view.dart | 187 ++++++++++++++++-- 5 files changed, 240 insertions(+), 21 deletions(-) diff --git a/flutter/_utils/autoreload.sh b/flutter/_utils/autoreload.sh index f602c8f..0823697 100755 --- a/flutter/_utils/autoreload.sh +++ b/flutter/_utils/autoreload.sh @@ -24,7 +24,7 @@ -pid="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' )" +pid="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' | tail -n 1 )" if [ -z "$pid" ]; then red "No [flutter run] process found - exiting" diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index 1484113..acabe3f 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -182,6 +182,16 @@ class APIClient { ); } + static Future getChannel(TokenSource auth, String cid) async { + return await _request( + name: 'getChannel', + method: 'GET', + relURL: 'users/${auth.getUserID()}/channels/${cid}', + fn: ChannelWithSubscription.fromJson, + authToken: auth.getToken(), + ); + } + static Future<(String, List)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List? channelIDs}) async { return await _request( name: 'getMessageList', diff --git a/flutter/lib/api/api_exception.dart b/flutter/lib/api/api_exception.dart index a0dbf41..d76125f 100644 --- a/flutter/lib/api/api_exception.dart +++ b/flutter/lib/api/api_exception.dart @@ -1,6 +1,6 @@ class APIException implements Exception { final int httpStatus; - final String error; + final int error; final String errHighlight; final String message; diff --git a/flutter/lib/models/api_error.dart b/flutter/lib/models/api_error.dart index adb6fb2..510d085 100644 --- a/flutter/lib/models/api_error.dart +++ b/flutter/lib/models/api_error.dart @@ -1,9 +1,61 @@ class APIError { - final String success; - final String error; + final bool success; + final int error; final String errhighlight; final String message; + static final MISSING_UID = 1101; + static final MISSING_TOK = 1102; + static final MISSING_TITLE = 1103; + static final INVALID_PRIO = 1104; + static final REQ_METHOD = 1105; + static final INVALID_CLIENTTYPE = 1106; + static final PAGETOKEN_ERROR = 1121; + static final BINDFAIL_QUERY_PARAM = 1151; + static final BINDFAIL_BODY_PARAM = 1152; + static final BINDFAIL_URI_PARAM = 1153; + static final INVALID_BODY_PARAM = 1161; + static final INVALID_ENUM_VALUE = 1171; + + static final NO_TITLE = 1201; + static final TITLE_TOO_LONG = 1202; + static final CONTENT_TOO_LONG = 1203; + static final USR_MSG_ID_TOO_LONG = 1204; + static final TIMESTAMP_OUT_OF_RANGE = 1205; + static final SENDERNAME_TOO_LONG = 1206; + static final CHANNEL_TOO_LONG = 1207; + static final CHANNEL_DESCRIPTION_TOO_LONG = 1208; + static final CHANNEL_NAME_EMPTY = 1209; + + static final USER_NOT_FOUND = 1301; + static final CLIENT_NOT_FOUND = 1302; + static final CHANNEL_NOT_FOUND = 1303; + static final SUBSCRIPTION_NOT_FOUND = 1304; + static final MESSAGE_NOT_FOUND = 1305; + static final SUBSCRIPTION_USER_MISMATCH = 1306; + static final KEY_NOT_FOUND = 1307; + static final USER_AUTH_FAILED = 1311; + + static final NO_DEVICE_LINKED = 1401; + + static final CHANNEL_ALREADY_EXISTS = 1501; + static final CANNOT_SELFDELETE_KEY = 1511; + static final CANNOT_SELFUPDATE_KEY = 1512; + + static final QUOTA_REACHED = 2101; + + static final FAILED_VERIFY_PRO_TOKEN = 3001; + static final INVALID_PRO_TOKEN = 3002; + + static final COMMIT_FAILED = 9001; + static final DATABASE_ERROR = 9002; + static final PERM_QUERY_FAIL = 9003; + static final FIREBASE_COM_FAILED = 9901; + static final FIREBASE_COM_ERRORED = 9902; + static final INTERNAL_EXCEPTION = 9903; + static final PANIC = 9904; + static final NOT_IMPLEMENTED = 9905; + const APIError({ required this.success, required this.error, @@ -13,8 +65,8 @@ class APIError { factory APIError.fromJson(Map json) { return APIError( - success: json['success'] as String, - error: json['error'] as String, + success: json['success'] as bool, + error: (json['error'] as double).toInt(), errhighlight: json['errhighlight'] as String, message: json['message'] as String, ); diff --git a/flutter/lib/pages/message_view/message_view.dart b/flutter/lib/pages/message_view/message_view.dart index c323b00..4bb62e5 100644 --- a/flutter/lib/pages/message_view/message_view.dart +++ b/flutter/lib/pages/message_view/message_view.dart @@ -1,8 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/api/api_exception.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; +import 'package:simplecloudnotifier/models/api_error.dart'; +import 'package:simplecloudnotifier/models/channel.dart'; +import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/message.dart'; +import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; class MessageViewPage extends StatefulWidget { @@ -15,18 +23,43 @@ class MessageViewPage extends StatefulWidget { } class _MessageViewPageState extends State { - late Future? futureMessage; + late Future<(Message, ChannelWithSubscription?, KeyToken?)>? mainFuture; + static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm'); @override void initState() { super.initState(); - futureMessage = fetchMessage(); + mainFuture = fetchData(); } - Future fetchMessage() async { + Future<(Message, ChannelWithSubscription?, KeyToken?)> fetchData() async { final acc = Provider.of(context, listen: false); - return await APIClient.getMessage(acc, widget.message.messageID); + final msg = await APIClient.getMessage(acc, widget.message.messageID); + + ChannelWithSubscription? chn = null; + try { + chn = await APIClient.getChannel(acc, msg.channelID); + } on APIException catch (e) { + if (e.error == APIError.USER_AUTH_FAILED) { + chn = null; + } else { + rethrow; + } + } + + KeyToken? tok = null; + try { + tok = await APIClient.getKeyToken(acc, msg.usedKeyID); + } on APIException catch (e) { + if (e.error == APIError.USER_AUTH_FAILED) { + tok = null; + } else { + rethrow; + } + } + + return (msg, chn, tok); } @override @@ -39,15 +72,19 @@ class _MessageViewPageState extends State { return SCNScaffold( title: 'Message', showSearch: false, - child: FutureBuilder( - future: futureMessage, + //TODO showShare: true + child: FutureBuilder<(Message, ChannelWithSubscription?, KeyToken?)>( + future: mainFuture, builder: (context, snapshot) { if (snapshot.hasData) { - return buildMessageView(snapshot.data!, false); + final msg = snapshot.data!.$1; + final chn = snapshot.data!.$2; + final tok = snapshot.data!.$3; + return _buildMessageView(context, msg, chn, tok); } else if (snapshot.hasError) { return Center(child: Text('${snapshot.error}')); //TODO nice error page } else if (!widget.message.trimmed) { - return buildMessageView(widget.message, true); + return _buildLoadingView(context, widget.message); } else { return const Center(child: CircularProgressIndicator()); } @@ -56,15 +93,135 @@ class _MessageViewPageState extends State { ); } - Widget buildMessageView(Message message, bool loading) { + Widget _buildMessageView(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token) { //TODO loading true/false indicator - return Center( - child: Column( - children: [ - Text(message.title), - Text(message.content ?? ''), - ], + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ..._buildMessageHeader(context, message, channel, token), + SizedBox(height: 8), + if (message.content != null) ..._buildMessageContent(context, message, channel, token), + SizedBox(height: 8), + if (message.senderName != null) _buildMetaCard(context, FontAwesomeIcons.solidSignature, 'Sender', [message.senderName!], () => {/*TODO*/}), + if (token != null) _buildMetaCard(context, FontAwesomeIcons.solidGearCode, 'KeyToken', [token.keytokenID, token.name], () => {/*TODO*/}), + _buildMetaCard(context, FontAwesomeIcons.solidIdCardClip, 'MessageID', [message.messageID, if (message.userMessageID != null) message.userMessageID!], null), + if (channel != null) _buildMetaCard(context, FontAwesomeIcons.solidSnake, 'Channel', [message.channelID, channel.channel.displayName], () => {/*TODO*/}), + _buildMetaCard(context, FontAwesomeIcons.solidTimer, 'Timestamp', [message.timestamp], null), + ], + ), ), ); } + + Widget _buildLoadingView(BuildContext context, Message message) { + //TODO loading / skeleton use limitdata + return SizedBox(); + } + + String _resolveChannelName(ChannelWithSubscription? channel, Message message) { + return channel?.channel.displayName ?? message.channelInternalName; + } + + List _buildMessageHeader(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token) { + return [ + Row( + children: [ + Container( + padding: const EdgeInsets.fromLTRB(4, 0, 4, 0), + margin: const EdgeInsets.fromLTRB(0, 0, 4, 0), + decoration: BoxDecoration( + color: Theme.of(context).hintColor, + borderRadius: BorderRadius.all(Radius.circular(4)), + ), + child: Text( + _resolveChannelName(channel, message), + style: TextStyle(fontWeight: FontWeight.bold, color: Theme.of(context).cardColor, fontSize: 16), + overflow: TextOverflow.clip, + maxLines: 1, + ), + ), + Expanded(child: SizedBox()), + Text(_dateFormat.format(DateTime.parse(message.timestamp)), style: const TextStyle(fontSize: 14)), + ], + ), + SizedBox(height: 8), + Text(message.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + ]; + } + + List _buildMessageContent(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token) { + return [ + Row( + children: [ + Expanded(child: SizedBox()), + IconButton( + icon: FaIcon(FontAwesomeIcons.copy), + iconSize: 18, + padding: EdgeInsets.all(4), + constraints: BoxConstraints(), + style: ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), + onPressed: () {/*TODO*/}, + ), + IconButton( + icon: FaIcon(FontAwesomeIcons.lineColumns), + iconSize: 18, + padding: EdgeInsets.all(4), + constraints: BoxConstraints(), + style: ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), + onPressed: () {/*TODO*/}, + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).hintColor), + borderRadius: BorderRadius.circular(4), + ), + padding: const EdgeInsets.all(4), + child: Text(message.content ?? ''), + ), + ]; + } + + Widget _buildMetaCard(BuildContext context, IconData icn, String title, List values, void Function()? action) { + final container = Container( + padding: EdgeInsets.fromLTRB(16, 2, 4, 2), + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).hintColor), + borderRadius: BorderRadius.circular(4), + ), + 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, + ), + ); + } + } }