diff --git a/lib/message_item_view.dart b/lib/message_item_view.dart index fee83e9..e64116a 100644 --- a/lib/message_item_view.dart +++ b/lib/message_item_view.dart @@ -1,18 +1,28 @@ +import 'package:enough_mail/enough_mail.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:wind/thread_model.dart'; class MessageItemView extends StatelessWidget { - const MessageItemView({Key? key, required this.item, required this.isOpPost}) + const MessageItemView( + {Key? key, + required this.item, + required this.isOpPost, + required this.isLast}) : super(key: key); final MessageItem item; final bool isOpPost; + final bool isLast; @override Widget build(BuildContext context) { return Container( - margin: this.isOpPost ? EdgeInsets.only(bottom: 10) : EdgeInsets.all(0), + margin: this.isOpPost + ? EdgeInsets.only(bottom: 10, left: 10, right: 10, top: 16) + : isLast + ? EdgeInsets.only(left: 10, right: 10, bottom: 16) + : EdgeInsets.only(left: 10, right: 10), child: Card( elevation: 5, child: Column( @@ -81,6 +91,8 @@ class MessageItem { final String date; final String body; + MimeMessage? originalMessage; + MessageItem( this.id, this.number, this.subject, this.author, this.date, this.body); } diff --git a/lib/nntp_client.dart b/lib/nntp_client.dart index 1482436..7cff1ad 100644 --- a/lib/nntp_client.dart +++ b/lib/nntp_client.dart @@ -6,8 +6,8 @@ import 'package:tuple/tuple.dart'; class NNTPClient { late WebSocketChannel _channel; - final Queue<_NNTPCommand> commandQueue = new Queue(); - final List tempBuffer = []; + final Queue<_NNTPCommand> _commandQueue = new Queue(); + final List _tempBuffer = []; String? currentGroup; @@ -25,22 +25,22 @@ class NNTPClient { var respLines = resp.split("\r\n"); if (respLines.last == "") respLines.removeLast(); // trailing empty line - if ((respLines.length > 1 || tempBuffer.isNotEmpty) && + if ((respLines.length > 1 || _tempBuffer.isNotEmpty) && respLines.last.codeUnits.last != ".".codeUnits.first) { // if it's multiline response and it doesn't contain dot in the end // then looks like we need to wait for next message to concatenate with current msg - tempBuffer.add(resp); + _tempBuffer.add(resp); return; } - if (tempBuffer.isNotEmpty) { - tempBuffer.add(resp); - resp = tempBuffer.join(); + if (_tempBuffer.isNotEmpty) { + _tempBuffer.add(resp); + resp = _tempBuffer.join(); respLines = resp.split("\r\n"); respLines.removeLast(); // trailing empty line - tempBuffer.clear(); + _tempBuffer.clear(); } - var command = commandQueue.removeFirst(); + var command = _commandQueue.removeFirst(); var respCode = int.parse(respLines[0].split(" ")[0]); command.responseCompleter.complete(_CommandResponse(respCode, respLines)); }); @@ -49,7 +49,7 @@ class NNTPClient { Future<_CommandResponse> _sendCommand( String command, List args) async { var cmd = _NNTPCommand(_CommandRequest(command, args)); - commandQueue.add(cmd); + _commandQueue.add(cmd); if (args.length > 0) { _channel.sink.add("$command ${args.join(" ")}\r\n"); } else { @@ -155,6 +155,18 @@ class NNTPClient { return threads; } + + Future postArticle(MimeMessage message) async { + var resp = await _sendCommand("POST", []); + if (resp.responseCode != 340) return resp.responseCode; + var rawMessage = message.renderMessage() + "\r\n.\r\n"; + + _channel.sink.add(rawMessage); + var cmd = _NNTPCommand(_CommandRequest("POST", [])); + _commandQueue.add(cmd); + resp = await cmd.response; + return resp.responseCode; + } } class _NNTPCommand { diff --git a/lib/thread_model.dart b/lib/thread_model.dart index 9608863..95c8e28 100644 --- a/lib/thread_model.dart +++ b/lib/thread_model.dart @@ -1,3 +1,4 @@ +import 'package:enough_mail/enough_mail.dart'; import 'package:flutter/cupertino.dart'; import 'package:wind/message_item_view.dart'; import 'package:wind/nntp_client.dart'; @@ -9,13 +10,15 @@ class ThreadModel extends ChangeNotifier { Future getPost(int number) async { var msg = await client!.getPost(number); - return MessageItem( + var mi = MessageItem( msg.getHeaderValue("Message-Id")!, number, msg.getHeaderValue("Subject")!, msg.getHeaderValue("From")!, msg.getHeaderValue("Date")!, msg.decodeTextPlainPart()!); + mi.originalMessage = msg; + return mi; } Future> getThread(int threadNumber) async { @@ -39,4 +42,15 @@ class ThreadModel extends ChangeNotifier { return items; } + + Future postMessage(MimeMessage opPost, String text) async { + var msg = MessageBuilder.buildSimpleTextMessage( + MailAddress.empty(), [], text, + subject: "Re: " + opPost.decodeSubject()!); + msg.setHeader("From", "anonymous"); + msg.addHeader("In-Reply-To", opPost.getHeaderValue("Message-Id")); + msg.addHeader("References", opPost.getHeaderValue("Message-Id")); + msg.addHeader("Newsgroups", client!.currentGroup!); + return await client!.postArticle(msg); + } } diff --git a/lib/thread_screen.dart b/lib/thread_screen.dart index efbca5d..3181723 100644 --- a/lib/thread_screen.dart +++ b/lib/thread_screen.dart @@ -1,7 +1,7 @@ +import 'package:enough_mail/mime_message.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:wind/message_item_view.dart'; -import 'package:wind/nntp_client.dart'; import 'package:wind/thread_list_view.dart'; import 'package:wind/thread_model.dart'; @@ -22,21 +22,17 @@ class ThreadScreen extends StatefulWidget { class ThreadScreenState extends State { ThreadScreenState(this.threadNumber); - - late NNTPClient client; late int threadNumber; @override Widget build(BuildContext context) { - client = context.read(); - return Scaffold( appBar: AppBar( title: Text("Thread #${this.threadNumber}"), ), body: Center( child: Container( - width: 640, + width: 650, child: FutureBuilder>( future: _fetch(context), builder: (context, snapshot) { @@ -71,25 +67,34 @@ class ThreadScreenState extends State { itemCount: data.length, itemBuilder: (context, index) { if (index == 1) { - return SendMessageForm(); + return SendMessageForm(opPost: data[0].originalMessage!); } - return MessageItemView(item: data[index], isOpPost: index == 0); + return MessageItemView( + item: data[index], + isOpPost: index == 0, + isLast: index == data.length - 1); }); } } class SendMessageForm extends StatefulWidget { - const SendMessageForm({Key? key}) : super(key: key); + const SendMessageForm({Key? key, required this.opPost}) : super(key: key); + + final MimeMessage opPost; @override SendMessageFormState createState() { - return SendMessageFormState(); + return SendMessageFormState(opPost); } } // Create a corresponding State class. // This class holds data related to the form. class SendMessageFormState extends State { + SendMessageFormState(this.opPost); + + final MimeMessage opPost; + // Create a global key that uniquely identifies the Form widget // and allows validation of the form. // @@ -102,50 +107,64 @@ class SendMessageFormState extends State { // Build a Form widget using the _formKey created above. return Form( key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Column(children: [ - Consumer( - builder: ((context, value, child) => TextFormField( - controller: value.commentTextController, - minLines: 5, - keyboardType: TextInputType.multiline, - maxLines: null, - decoration: InputDecoration( - border: OutlineInputBorder(), labelText: "Comment"), - // The validator receives the text that the user has entered. - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter some text'; - } - return null; - }, - ))), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - ElevatedButton( - onPressed: () { - // Validate returns true if the form is valid, or false otherwise. - if (_formKey.currentState!.validate()) { - // If the form is valid, display a snackbar. In the real world, - // you'd often call a server or save the information in a database. - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Processing Data')), - ); + child: Container( + margin: EdgeInsets.only(left: 16, right: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Column(children: [ + Consumer( + builder: ((context, value, child) => TextFormField( + controller: value.commentTextController, + minLines: 5, + keyboardType: TextInputType.multiline, + maxLines: null, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: "Комментарий"), + // The validator receives the text that the user has entered. + validator: (value) { + if (value == null || value.isEmpty) { + return 'Пожалуйста, введите текст'; } + return null; }, - child: const Text('Send'), - ) - ])) - ]), - ) - ], + ))), + Consumer( + builder: (((context, value, child) => Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: () { + // Validate returns true if the form is valid, or false otherwise. + if (_formKey.currentState!.validate()) { + Provider.of(context, + listen: false) + .postMessage(opPost, + value.commentTextController.text) + .then((value) { + if (value == 240) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + content: Text('Пост отправлен!')), + ); + } + }); + value.commentTextController.text = ""; + } + }, + child: const Text('Отправить'), + ) + ])))), + ) + ]), + ) + ], + ), ), ); }