kopia lustrzana https://gitlab.com/mysocialportal/relatica
Add initial DM reply writing
rodzic
bdb01e5f26
commit
7611ac3097
|
@ -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,
|
||||
|
|
|
@ -38,6 +38,7 @@ enum ErrorType {
|
|||
missingEndpoint,
|
||||
notFound,
|
||||
parsingError,
|
||||
serverError,
|
||||
}
|
||||
|
||||
extension ExecErrorExtension<T, E> on Result<T, E> {
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue