2023-01-25 01:53:55 +00:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:provider/provider.dart';
|
2023-01-30 15:54:46 +00:00
|
|
|
import 'package:relatica/utils/clipboard_utils.dart';
|
2023-01-25 01:53:55 +00:00
|
|
|
import 'package:result_monad/result_monad.dart';
|
|
|
|
|
|
|
|
import '../controls/image_control.dart';
|
2023-01-25 18:06:46 +00:00
|
|
|
import '../controls/padding.dart';
|
2023-01-25 01:53:55 +00:00
|
|
|
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';
|
2023-01-25 18:06:46 +00:00
|
|
|
import '../utils/snackbar_builder.dart';
|
2023-01-25 01:53:55 +00:00
|
|
|
|
2023-01-25 18:06:46 +00:00
|
|
|
class MessageThreadScreen extends StatefulWidget {
|
2023-01-25 01:53:55 +00:00
|
|
|
final String parentThreadId;
|
|
|
|
|
|
|
|
const MessageThreadScreen({
|
|
|
|
super.key,
|
|
|
|
required this.parentThreadId,
|
|
|
|
});
|
|
|
|
|
2023-01-25 18:06:46 +00:00
|
|
|
@override
|
|
|
|
State<MessageThreadScreen> createState() => _MessageThreadScreenState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _MessageThreadScreenState extends State<MessageThreadScreen> {
|
|
|
|
final textController = TextEditingController();
|
|
|
|
|
2023-01-25 01:53:55 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
final service = context.watch<DirectMessageService>();
|
2023-01-25 18:06:46 +00:00
|
|
|
final result = service.getThreadByParentUri(widget.parentThreadId);
|
2023-01-25 01:53:55 +00:00
|
|
|
final title = result.fold(
|
|
|
|
onSuccess: (t) => t.title.isEmpty ? 'Thread' : t.title,
|
|
|
|
onError: (_) => 'Thread');
|
|
|
|
return Scaffold(
|
|
|
|
appBar: StandardAppBar.build(context, title),
|
2023-01-25 17:26:29 +00:00
|
|
|
body: buildBody(result, service),
|
2023-01-25 01:53:55 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-01-25 17:26:29 +00:00
|
|
|
Widget buildBody(
|
|
|
|
Result<DirectMessageThread, ExecError> result,
|
|
|
|
DirectMessageService service,
|
|
|
|
) {
|
2023-01-25 01:53:55 +00:00
|
|
|
return result.fold(
|
|
|
|
onSuccess: (thread) {
|
2023-02-27 03:12:40 +00:00
|
|
|
final yourId = getIt<AccountsService>().currentProfile.userId;
|
|
|
|
final yourAvatarUrl = getIt<AccountsService>().currentProfile.avatar;
|
2023-01-25 01:53:55 +00:00
|
|
|
final participants =
|
|
|
|
Map.fromEntries(thread.participants.map((p) => MapEntry(p.id, p)));
|
|
|
|
return Center(
|
2023-01-25 18:06:46 +00:00
|
|
|
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
|
2023-01-25 17:26:29 +00:00
|
|
|
? null
|
2023-01-25 18:06:46 +00:00
|
|
|
: () =>
|
|
|
|
service.markMessageRead(widget.parentThreadId, m),
|
2023-01-30 15:54:46 +00:00
|
|
|
onLongPress: () async {
|
|
|
|
await copyToClipboard(context: context, text: m.text);
|
|
|
|
},
|
2023-01-25 18:06:46 +00:00
|
|
|
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(
|
2023-01-30 15:54:46 +00:00
|
|
|
color: Theme.of(context).colorScheme.background,
|
2023-01-25 17:26:29 +00:00
|
|
|
),
|
2023-01-25 18:06:46 +00:00
|
|
|
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(),
|
|
|
|
],
|
|
|
|
));
|
2023-01-25 01:53:55 +00:00
|
|
|
},
|
|
|
|
onError: (error) => Center(
|
|
|
|
child: Text('Error getting thread: $error'),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|