a bit of work on the message page
This commit is contained in:
parent
549311535c
commit
95d51c82e9
@ -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
|
if [ -z "$pid" ]; then
|
||||||
red "No [flutter run] process found - exiting"
|
red "No [flutter run] process found - exiting"
|
||||||
|
@ -182,6 +182,16 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<ChannelWithSubscription> 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<Message>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? channelIDs}) async {
|
static Future<(String, List<Message>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? channelIDs}) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'getMessageList',
|
name: 'getMessageList',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
class APIException implements Exception {
|
class APIException implements Exception {
|
||||||
final int httpStatus;
|
final int httpStatus;
|
||||||
final String error;
|
final int error;
|
||||||
final String errHighlight;
|
final String errHighlight;
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
|
@ -1,9 +1,61 @@
|
|||||||
class APIError {
|
class APIError {
|
||||||
final String success;
|
final bool success;
|
||||||
final String error;
|
final int error;
|
||||||
final String errhighlight;
|
final String errhighlight;
|
||||||
final String message;
|
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({
|
const APIError({
|
||||||
required this.success,
|
required this.success,
|
||||||
required this.error,
|
required this.error,
|
||||||
@ -13,8 +65,8 @@ class APIError {
|
|||||||
|
|
||||||
factory APIError.fromJson(Map<String, dynamic> json) {
|
factory APIError.fromJson(Map<String, dynamic> json) {
|
||||||
return APIError(
|
return APIError(
|
||||||
success: json['success'] as String,
|
success: json['success'] as bool,
|
||||||
error: json['error'] as String,
|
error: (json['error'] as double).toInt(),
|
||||||
errhighlight: json['errhighlight'] as String,
|
errhighlight: json['errhighlight'] as String,
|
||||||
message: json['message'] as String,
|
message: json['message'] as String,
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
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:provider/provider.dart';
|
||||||
import 'package:simplecloudnotifier/api/api_client.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/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/message.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/user.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
|
|
||||||
class MessageViewPage extends StatefulWidget {
|
class MessageViewPage extends StatefulWidget {
|
||||||
@ -15,18 +23,43 @@ class MessageViewPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MessageViewPageState extends State<MessageViewPage> {
|
class _MessageViewPageState extends State<MessageViewPage> {
|
||||||
late Future<Message>? futureMessage;
|
late Future<(Message, ChannelWithSubscription?, KeyToken?)>? mainFuture;
|
||||||
|
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
futureMessage = fetchMessage();
|
mainFuture = fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Message> fetchMessage() async {
|
Future<(Message, ChannelWithSubscription?, KeyToken?)> fetchData() async {
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
final acc = Provider.of<AppAuth>(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
|
@override
|
||||||
@ -39,15 +72,19 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
return SCNScaffold(
|
return SCNScaffold(
|
||||||
title: 'Message',
|
title: 'Message',
|
||||||
showSearch: false,
|
showSearch: false,
|
||||||
child: FutureBuilder<Message>(
|
//TODO showShare: true
|
||||||
future: futureMessage,
|
child: FutureBuilder<(Message, ChannelWithSubscription?, KeyToken?)>(
|
||||||
|
future: mainFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData) {
|
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) {
|
} else if (snapshot.hasError) {
|
||||||
return Center(child: Text('${snapshot.error}')); //TODO nice error page
|
return Center(child: Text('${snapshot.error}')); //TODO nice error page
|
||||||
} else if (!widget.message.trimmed) {
|
} else if (!widget.message.trimmed) {
|
||||||
return buildMessageView(widget.message, true);
|
return _buildLoadingView(context, widget.message);
|
||||||
} else {
|
} else {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
@ -56,15 +93,135 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildMessageView(Message message, bool loading) {
|
Widget _buildMessageView(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token) {
|
||||||
//TODO loading true/false indicator
|
//TODO loading true/false indicator
|
||||||
return Center(
|
return SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(message.title),
|
..._buildMessageHeader(context, message, channel, token),
|
||||||
Text(message.content ?? ''),
|
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<Widget> _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<Widget> _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<String> 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user