334 lines
12 KiB
Dart
334 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:share_plus/share_plus.dart';
|
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
|
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
|
|
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
|
import 'package:simplecloudnotifier/models/channel.dart';
|
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
|
import 'package:simplecloudnotifier/models/scn_message.dart';
|
|
import 'package:simplecloudnotifier/models/user.dart';
|
|
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
|
|
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
|
|
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_view.dart';
|
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
|
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
|
import 'package:simplecloudnotifier/state/app_settings.dart';
|
|
import 'package:simplecloudnotifier/utils/navi.dart';
|
|
import 'package:simplecloudnotifier/utils/toaster.dart';
|
|
import 'package:simplecloudnotifier/utils/ui.dart';
|
|
|
|
class MessageViewPage extends StatefulWidget {
|
|
const MessageViewPage({
|
|
super.key,
|
|
required this.messageID,
|
|
required this.preloadedData,
|
|
});
|
|
|
|
final String messageID; // Potentially trimmed
|
|
final (SCNMessage,)? preloadedData; // Message is potentially trimmed, whole object is potentially null
|
|
|
|
@override
|
|
State<MessageViewPage> createState() => _MessageViewPageState();
|
|
}
|
|
|
|
class _MessageViewPageState extends State<MessageViewPage> {
|
|
late Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture;
|
|
(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null;
|
|
|
|
final ScrollController _controller = ScrollController();
|
|
|
|
bool _monospaceMode = false;
|
|
|
|
SCNMessage? message = null;
|
|
|
|
@override
|
|
void initState() {
|
|
if (widget.preloadedData != null) {
|
|
message = widget.preloadedData!.$1;
|
|
}
|
|
|
|
mainFuture = fetchData();
|
|
super.initState();
|
|
}
|
|
|
|
Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)> fetchData() async {
|
|
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 acc = Provider.of<AppAuth>(context, listen: false);
|
|
|
|
final msg = await APIClient.getMessage(acc, widget.messageID);
|
|
|
|
final fut_chn = APIClient.getChannelPreview(acc, msg.channelID);
|
|
final fut_key = APIClient.getKeyTokenPreviewByID(acc, msg.usedKeyID);
|
|
final fut_usr = APIClient.getUserPreview(acc, msg.senderUserID);
|
|
|
|
final chn = await fut_chn;
|
|
final key = await fut_key;
|
|
final usr = await fut_usr;
|
|
|
|
//await Future.delayed(const Duration(seconds: 10), () {});
|
|
|
|
final r = (msg, chn, key, usr);
|
|
|
|
mainFutureSnapshot = r;
|
|
|
|
return r;
|
|
} finally {
|
|
AppBarState().setLoadingIndeterminate(false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SCNScaffold(
|
|
title: 'Message',
|
|
showSearch: false,
|
|
showShare: true,
|
|
onShare: _share,
|
|
child: FutureBuilder<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>(
|
|
future: mainFuture,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.hasData) {
|
|
final (msg, chn, tok, usr) = snapshot.data!;
|
|
return _buildMessageView(context, msg, chn, tok, usr);
|
|
} else if (snapshot.hasError) {
|
|
return ErrorDisplay(errorMessage: '${snapshot.error}');
|
|
} else if (message != null && !this.message!.trimmed) {
|
|
return _buildMessageView(context, this.message!, null, null, null);
|
|
} else {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
void _share() async {
|
|
if (this.message == null) return;
|
|
|
|
var msg = this.message!;
|
|
if (mainFutureSnapshot != null) {
|
|
(msg, _, _, _) = mainFutureSnapshot!;
|
|
}
|
|
|
|
if (msg.content != null) {
|
|
final result = await Share.share(msg.content!, subject: msg.title);
|
|
|
|
if (result.status == ShareResultStatus.unavailable) {
|
|
Toaster.error('Error', "Failed to open share dialog");
|
|
}
|
|
} else {
|
|
final result = await Share.share(msg.title);
|
|
|
|
if (result.status == ShareResultStatus.unavailable) {
|
|
Toaster.error('Error', "Failed to open share dialog");
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget _buildMessageView(BuildContext context, SCNMessage message, ChannelPreview? channel, KeyTokenPreview? token, UserPreview? user) {
|
|
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
|
|
|
|
final child = Padding(
|
|
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
..._buildMessageHeader(context, message, channel),
|
|
SizedBox(height: 8),
|
|
if (message.content != null) ..._buildMessageContent(context, message),
|
|
SizedBox(height: 8),
|
|
if (message.senderName != null)
|
|
UI.metaCard(
|
|
context: context,
|
|
icon: FontAwesomeIcons.solidSignature,
|
|
title: 'Sender',
|
|
values: [message.senderName!],
|
|
mainAction: () => {
|
|
Navi.push(context, () => FilteredMessageViewPage(title: message.senderName!, filter: MessageFilter(senderNames: [message.senderName!])))
|
|
},
|
|
),
|
|
UI.metaCard(
|
|
context: context,
|
|
icon: FontAwesomeIcons.solidGearCode,
|
|
title: 'KeyToken',
|
|
values: [message.usedKeyID, token?.name ?? '...'],
|
|
mainAction: () {
|
|
if (message.senderUserID == userAccUserID) {
|
|
Navi.push(context, () => KeyTokenViewPage(keytokenID: message.usedKeyID, preloadedData: null, needsReload: null));
|
|
} else {
|
|
Navi.push(context, () => FilteredMessageViewPage(title: token?.name ?? message.usedKeyID, filter: MessageFilter(usedKeys: [message.usedKeyID])));
|
|
}
|
|
},
|
|
),
|
|
UI.metaCard(
|
|
context: context,
|
|
icon: FontAwesomeIcons.solidIdCardClip,
|
|
title: 'MessageID',
|
|
values: [message.messageID, message.userMessageID ?? ''],
|
|
),
|
|
UI.metaCard(
|
|
context: context,
|
|
icon: FontAwesomeIcons.solidSnake,
|
|
title: 'Channel',
|
|
values: [message.channelID, channel?.displayName ?? message.channelInternalName],
|
|
mainAction: (channel != null)
|
|
? () {
|
|
Navi.push(context, () => ChannelViewPage(channelID: channel.channelID, preloadedData: null, needsReload: null));
|
|
}
|
|
: null,
|
|
),
|
|
UI.metaCard(
|
|
context: context,
|
|
icon: FontAwesomeIcons.solidTimer,
|
|
title: 'Timestamp',
|
|
values: [message.timestamp],
|
|
),
|
|
UI.metaCard(
|
|
context: context,
|
|
icon: FontAwesomeIcons.solidUser,
|
|
title: 'User',
|
|
values: [user?.userID ?? message.senderUserID, user?.username ?? ''],
|
|
mainAction: () => FilteredMessageViewPage(title: user?.username ?? message.senderUserID, filter: MessageFilter(senderUserID: [message.senderUserID])),
|
|
),
|
|
UI.metaCard(
|
|
context: context,
|
|
icon: FontAwesomeIcons.solidBolt,
|
|
title: 'Priority',
|
|
values: [_prettyPrintPriority(message.priority)],
|
|
mainAction: () => FilteredMessageViewPage(title: "Priority ${message.priority}", filter: MessageFilter(priority: [message.priority])),
|
|
),
|
|
if (message.senderUserID == userAccUserID) UI.button(text: "Delete Message", onPressed: () {/*TODO*/}, color: Colors.red[900]),
|
|
],
|
|
),
|
|
);
|
|
|
|
var showScrollbar = false;
|
|
if (!_monospaceMode && (message.content ?? '').length > 4096) showScrollbar = true;
|
|
if (_monospaceMode && (message.content ?? '').split('\n').length > 64) showScrollbar = true;
|
|
|
|
if (showScrollbar) {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(0, 0, 6, 0),
|
|
child: Scrollbar(
|
|
thickness: 12.0,
|
|
radius: Radius.circular(6),
|
|
thumbVisibility: false,
|
|
interactive: true,
|
|
controller: _controller,
|
|
child: SingleChildScrollView(
|
|
controller: _controller,
|
|
child: child,
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
return SingleChildScrollView(
|
|
child: child,
|
|
);
|
|
}
|
|
}
|
|
|
|
String _resolveChannelName(ChannelPreview? channel, SCNMessage message) {
|
|
return channel?.displayName ?? message.channelInternalName;
|
|
}
|
|
|
|
List<Widget> _buildMessageHeader(BuildContext context, SCNMessage message, ChannelPreview? channel) {
|
|
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
|
|
|
|
return [
|
|
Row(
|
|
children: [
|
|
UI.channelChip(
|
|
context: context,
|
|
text: _resolveChannelName(channel, message),
|
|
margin: const EdgeInsets.fromLTRB(0, 0, 4, 0),
|
|
fontSize: 16,
|
|
),
|
|
Expanded(child: SizedBox()),
|
|
Text(dateFormat.format(DateTime.parse(message.timestamp)), style: const TextStyle(fontSize: 14)),
|
|
],
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(_preformatTitle(message), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
|
];
|
|
}
|
|
|
|
List<Widget> _buildMessageContent(BuildContext context, SCNMessage message) {
|
|
return [
|
|
Row(
|
|
children: [
|
|
if (message.priority == 2) FaIcon(FontAwesomeIcons.solidTriangleExclamation, size: 16, color: Colors.red[900]),
|
|
if (message.priority == 0) FaIcon(FontAwesomeIcons.solidDown, size: 16, color: Colors.lightBlue[900]),
|
|
Expanded(child: SizedBox()),
|
|
UI.buttonIconOnly(
|
|
onPressed: () {
|
|
Clipboard.setData(new ClipboardData(text: message.content ?? ''));
|
|
Toaster.info("Clipboard", 'Copied text to Clipboard');
|
|
print('================= [CLIPBOARD] =================\n${message.content}\n================= [/CLIPBOARD] =================');
|
|
},
|
|
icon: FontAwesomeIcons.copy,
|
|
),
|
|
UI.buttonIconOnly(
|
|
icon: _monospaceMode ? FontAwesomeIcons.lineColumns : FontAwesomeIcons.alignLeft,
|
|
onPressed: () {
|
|
setState(() {
|
|
_monospaceMode = !_monospaceMode;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
_monospaceMode
|
|
? UI.box(
|
|
context: context,
|
|
padding: const EdgeInsets.all(4),
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Text(
|
|
message.content ?? '',
|
|
style: TextStyle(fontFamily: "monospace", fontFamilyFallback: <String>["Courier"]),
|
|
),
|
|
),
|
|
borderColor: (message.priority == 2) ? Colors.red[900] : null,
|
|
)
|
|
: UI.box(
|
|
context: context,
|
|
padding: const EdgeInsets.all(4),
|
|
child: Text(message.content ?? ''),
|
|
borderColor: (message.priority == 2) ? Colors.red[900] : null,
|
|
)
|
|
];
|
|
}
|
|
|
|
String _preformatTitle(SCNMessage message) {
|
|
return message.title.replaceAll('\n', '').replaceAll('\r', '').replaceAll('\t', ' ');
|
|
}
|
|
|
|
String _prettyPrintPriority(int priority) {
|
|
switch (priority) {
|
|
case 0:
|
|
return 'Low (0)';
|
|
case 1:
|
|
return 'Normal (1)';
|
|
case 2:
|
|
return 'High (2)';
|
|
default:
|
|
return 'Unknown ($priority)';
|
|
}
|
|
}
|
|
}
|