Add initial DM reply writing

merge-requests/67/merge
Hank Grabowski 2023-01-25 12:06:46 -06:00
rodzic bdb01e5f26
commit 7611ac3097
5 zmienionych plików z 166 dodań i 35 usunięć

Wyświetl plik

@ -545,6 +545,32 @@ class FriendicaClient {
return result.execErrorCast();
}
FutureResult<DirectMessage, ExecError> postDirectMessage(
String? messageIdRepliedTo,
String receivingUserId,
String text,
) async {
final url = Uri.parse('https://$serverName/api/direct_messages/new');
final body = {
'user_id': receivingUserId,
'text': text,
if (messageIdRepliedTo != null) 'replyto': messageIdRepliedTo,
};
final result = await _postUrl(url, body)
.andThenAsync<DirectMessage, ExecError>((jsonString) async {
final json = jsonDecode(jsonString) as Map<String, dynamic>;
if (json.containsKey('error')) {
return buildErrorResult(
type: ErrorType.serverError,
message: "Error from server: ${json['error']}");
}
return Result.ok(
DirectMessageFriendicaExtension.fromJson(jsonDecode(jsonString)));
});
return result.execErrorCast();
}
FutureResult<PagedResponse<String>, ExecError> _getUrl(Uri url) async {
_logger.finer('GET: $url');
try {
@ -573,7 +599,7 @@ class FriendicaClient {
FutureResult<String, ExecError> _postUrl(
Uri url, Map<String, dynamic> body) async {
_logger.finer('POST: $url');
_logger.finer('POST: $url \n Body: $body');
try {
final response = await http.post(
url,

Wyświetl plik

@ -38,6 +38,7 @@ enum ErrorType {
missingEndpoint,
notFound,
parsingError,
serverError,
}
extension ExecErrorExtension<T, E> on Result<T, E> {

Wyświetl plik

@ -3,14 +3,16 @@ import 'package:provider/provider.dart';
import 'package:result_monad/result_monad.dart';
import '../controls/image_control.dart';
import '../controls/padding.dart';
import '../controls/standard_appbar.dart';
import '../globals.dart';
import '../models/direct_message_thread.dart';
import '../models/exec_error.dart';
import '../services/auth_service.dart';
import '../services/direct_message_service.dart';
import '../utils/snackbar_builder.dart';
class MessageThreadScreen extends StatelessWidget {
class MessageThreadScreen extends StatefulWidget {
final String parentThreadId;
const MessageThreadScreen({
@ -18,10 +20,17 @@ class MessageThreadScreen extends StatelessWidget {
required this.parentThreadId,
});
@override
State<MessageThreadScreen> createState() => _MessageThreadScreenState();
}
class _MessageThreadScreenState extends State<MessageThreadScreen> {
final textController = TextEditingController();
@override
Widget build(BuildContext context) {
final service = context.watch<DirectMessageService>();
final result = service.getThreadByParentUri(parentThreadId);
final result = service.getThreadByParentUri(widget.parentThreadId);
final title = result.fold(
onSuccess: (t) => t.title.isEmpty ? 'Thread' : t.title,
onError: (_) => 'Thread');
@ -42,38 +51,94 @@ class MessageThreadScreen extends StatelessWidget {
final participants =
Map.fromEntries(thread.participants.map((p) => MapEntry(p.id, p)));
return Center(
child: ListView.separated(
itemBuilder: (context, index) {
final m = thread.messages[index];
final textPieces = m.text.split('...\n');
final text =
textPieces.length == 1 ? textPieces[0] : textPieces[1];
final imageUrl = m.senderId == yourId
? yourAvatarUrl
: participants[m.senderId]?.avatarUrl ?? '';
return ListTile(
onTap: m.seen
? null
: () => service.markMessageRead(parentThreadId, m),
leading: ImageControl(
imageUrl: imageUrl,
iconOverride: const Icon(Icons.person),
width: 32.0,
onTap: null,
),
title: Text(
text,
style: m.seen
child: Column(
children: [
Expanded(
child: ListView.separated(
itemBuilder: (context, index) {
final m = thread.messages[index];
final textPieces = m.text.split('...\n');
final text =
textPieces.length == 1 ? textPieces[0] : textPieces[1];
final imageUrl = m.senderId == yourId
? yourAvatarUrl
: participants[m.senderId]?.avatarUrl ?? '';
return ListTile(
onTap: m.seen
? null
: TextStyle(fontWeight: FontWeight.bold),
: () =>
service.markMessageRead(widget.parentThreadId, m),
leading: ImageControl(
imageUrl: imageUrl,
iconOverride: const Icon(Icons.person),
width: 32.0,
onTap: null,
),
title: Text(
text,
style: m.seen
? null
: const TextStyle(fontWeight: FontWeight.bold),
),
trailing: Text(DateTime.fromMillisecondsSinceEpoch(
m.createdAt * 1000)
.toString()),
);
},
separatorBuilder: (_, __) => const Divider(),
itemCount: thread.messages.length),
),
const VerticalDivider(),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
controller: textController,
maxLines: 4,
decoration: InputDecoration(
labelText: 'Reply Text',
border: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).backgroundColor,
),
trailing: Text(
DateTime.fromMillisecondsSinceEpoch(m.createdAt * 1000)
.toString()),
);
},
separatorBuilder: (_, __) => const Divider(),
itemCount: thread.messages.length));
borderRadius: BorderRadius.circular(5.0),
),
),
),
),
ElevatedButton(
onPressed: () async {
if (textController.text.isEmpty) {
buildSnackbar(context, "Can't submit an empty reply");
return;
}
final othersMessages =
thread.messages.where((m) => m.senderId != yourId);
if (othersMessages.isEmpty) {
buildSnackbar(
context, "Have to wait for a response before sending");
return;
}
await service
.newReplyMessage(
thread.parentUri,
othersMessages.last,
textController.text,
)
.match(onSuccess: (_) {
setState(() {
textController.clear();
});
}, onError: (error) {
if (mounted) {
buildSnackbar(context, error.message);
}
});
},
child: const Text('Submit'),
),
const VerticalPadding(),
],
));
},
onError: (error) => Center(
child: Text('Error getting thread: $error'),

Wyświetl plik

@ -25,6 +25,8 @@ class MessagesScreen extends StatelessWidget {
Widget buildBody(BuildContext context, DirectMessageService service) {
final threads = service.threads;
threads.sort((t1, t2) =>
t2.messages.last.createdAt.compareTo(t1.messages.last.createdAt));
return threads.isEmpty
? const Text('No Direct Message Threads')
: ListView.separated(
@ -59,7 +61,7 @@ class MessagesScreen extends StatelessWidget {
),
trailing: Text(
ElapsedDateUtils.epochSecondsToString(
thread.messages.first.createdAt),
thread.messages.last.createdAt),
style: style,
),
);

Wyświetl plik

@ -54,6 +54,44 @@ class DirectMessageService extends ChangeNotifier {
notifyListeners();
}
FutureResult<DirectMessage, ExecError> newReplyMessage(
String threadId, DirectMessage original, String text) async {
final thread = _threads[threadId];
if (thread == null) {
final error = 'Message is not for this thread: $threadId, $original';
_logger.severe(error);
return buildErrorResult(
type: ErrorType.notFound,
message: error,
);
}
if (!thread.messages.contains(original)) {
final error = 'Message is not for this thread: $threadId, $original';
_logger.severe(error);
return buildErrorResult(
type: ErrorType.notFound,
message: error,
);
}
final result = await getIt<AuthService>()
.currentClient
.andThenAsync((client) => client.postDirectMessage(
original.id,
original.senderId,
text,
));
result.match(onSuccess: (newMessage) {
thread.messages.add(newMessage);
notifyListeners();
}, onError: (error) {
_logger.severe('Error getting direct messages: $error');
});
return result.execErrorCast();
}
Future<void> markMessageRead(String threadId, DirectMessage m) async {
final thread = _threads[threadId];
if (thread == null) {
@ -79,6 +117,5 @@ class DirectMessageService extends ChangeNotifier {
_logger.severe('Error getting direct messages: $error');
},
);
notifyListeners();
}
}