From b6b7f69d5e177da4bddd5671eaaf1f075ce298bb Mon Sep 17 00:00:00 2001 From: bvlourenco Date: Sat, 19 Nov 2022 16:49:47 +0000 Subject: [PATCH] feat: can edit and delete comments from threads #298 --- frontend/lib/components/appbar.dart | 5 - frontend/lib/components/threadCard.dart | 575 ------------------ .../{ => threads}/addThreadForm.dart | 18 +- .../lib/components/threads/commentStrip.dart | 222 +++++++ .../lib/components/threads/editPostForm.dart | 94 +++ .../participations/communicationsList.dart | 44 ++ .../participationThreadsWidget.dart | 68 +++ .../lib/components/threads/threadCard.dart | 96 +++ .../components/threads/threadCardBody.dart | 185 ++++++ .../components/threads/threadCardHeader.dart | 135 ++++ .../lib/routes/company/CompanyScreen.dart | 7 +- .../routes/meeting/AddMeetingMemberForm.dart | 1 - .../lib/routes/meeting/MeetingScreen.dart | 15 +- .../lib/routes/speaker/SpeakerScreen.dart | 7 +- .../lib/routes/teams/AddTeamMemberForm.dart | 1 - frontend/lib/services/companyService.dart | 15 + frontend/lib/services/meetingService.dart | 15 + frontend/lib/services/speakerService.dart | 16 +- 18 files changed, 923 insertions(+), 596 deletions(-) delete mode 100644 frontend/lib/components/threadCard.dart rename frontend/lib/components/{ => threads}/addThreadForm.dart (94%) create mode 100644 frontend/lib/components/threads/commentStrip.dart create mode 100644 frontend/lib/components/threads/editPostForm.dart create mode 100644 frontend/lib/components/threads/participations/communicationsList.dart create mode 100644 frontend/lib/components/threads/participations/participationThreadsWidget.dart create mode 100644 frontend/lib/components/threads/threadCard.dart create mode 100644 frontend/lib/components/threads/threadCardBody.dart create mode 100644 frontend/lib/components/threads/threadCardHeader.dart diff --git a/frontend/lib/components/appbar.dart b/frontend/lib/components/appbar.dart index e8d5230c..ddf1cb00 100644 --- a/frontend/lib/components/appbar.dart +++ b/frontend/lib/components/appbar.dart @@ -2,12 +2,7 @@ import 'package:flutter/material.dart'; import 'package:frontend/components/SearchResultWidget.dart'; import 'package:frontend/components/deckTheme.dart'; import 'package:frontend/components/eventNotifier.dart'; -import 'package:frontend/components/SearchResultWidget.dart'; -import 'package:frontend/main.dart'; import 'package:frontend/models/event.dart'; -import 'package:frontend/routes/company/CompanyScreen.dart'; -import 'package:frontend/routes/member/MemberScreen.dart'; -import 'package:frontend/routes/speaker/SpeakerScreen.dart'; import 'package:frontend/services/eventService.dart'; import 'package:provider/provider.dart'; import 'package:frontend/models/company.dart'; diff --git a/frontend/lib/components/threadCard.dart b/frontend/lib/components/threadCard.dart deleted file mode 100644 index 479cc97b..00000000 --- a/frontend/lib/components/threadCard.dart +++ /dev/null @@ -1,575 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:frontend/components/deckTheme.dart'; -import 'package:frontend/models/member.dart'; -import 'package:frontend/models/participation.dart'; -import 'package:frontend/models/post.dart'; -import 'package:frontend/models/thread.dart'; -import 'package:frontend/services/threadService.dart'; -import 'package:provider/provider.dart'; -import 'package:shimmer/shimmer.dart'; -import 'package:intl/intl.dart'; - -final Map THREADCOLOR = { - "APPROVED": Colors.green, - "REVIEWED": Colors.green, - "PENDING": Colors.yellow, -}; - -class CommunicationsList extends StatelessWidget { - final List participations; - final bool small; - - CommunicationsList( - {Key? key, required this.participations, required this.small}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - return Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 20), - child: ListView( - controller: ScrollController(), - children: participations.reversed - .where((element) => - element.communicationsId != null && - element.communicationsId!.length != 0) - .map( - (participation) => ParticipationThreadsWidget( - participation: participation, - small: small, - ), - ) - .toList()), - ); - }); - } -} - -class ParticipationThreadsWidget extends StatelessWidget { - final Participation participation; - final bool small; - - ParticipationThreadsWidget( - {Key? key, required this.participation, required this.small}) - : super(key: key); - - @override - Widget build(BuildContext context) { - print('Getting ${participation.event}'); - return Padding( - padding: const EdgeInsets.all(8.0), - child: FutureBuilder( - future: participation.communications, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Text('Error'); - } - if (snapshot.connectionState == ConnectionState.done) { - List? threads = snapshot.data as List?; - if (threads == null) { - threads = []; - } - threads.sort((a, b) => b.posted.compareTo(a.posted)); - return Column(children: [ - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text('SINFO ${participation.event}'), - ), - ), - Divider(), - ...threads - .map( - (thread) => Padding( - padding: const EdgeInsets.all(8.0), - child: ThreadCard( - thread: thread, - small: small, - ), - ), - ) - .toList(), - ]); - } else { - return Center(child: CircularProgressIndicator()); - } - }, - ), - ); - } -} - -class ThreadCard extends StatefulWidget { - final Thread thread; - final bool small; - const ThreadCard({ - Key? key, - required this.thread, - required this.small, - }) : super(key: key); - - @override - ThreadCardState createState() => ThreadCardState(); -} - -class ThreadCardState extends State - with AutomaticKeepAliveClientMixin { - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - print(widget.thread.posted.toString()); - return FutureBuilder( - future: widget.thread.entry, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.hasError) { - return Text('Something went wrong'); - } - Post p = snapshot.data as Post; - return Padding( - padding: const EdgeInsets.all(8), - child: Container( - margin: EdgeInsets.fromLTRB(0, 20, 0, 0), - padding: EdgeInsets.fromLTRB(17, 15, 17, 15), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(5)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ThreadCardHeader( - p: p, thread: widget.thread, small: widget.small), - SizedBox( - height: 16, - ), - ThreadCardBody( - thread: widget.thread, - post: p, - small: widget.small, - ) - ], - ), - ), - ); - } else { - return Shimmer.fromColors( - baseColor: Colors.grey[400]!, - highlightColor: Colors.white, - child: Container( - margin: EdgeInsets.fromLTRB(0, 20, 0, 0), - padding: EdgeInsets.fromLTRB(17, 15, 17, 15), - decoration: BoxDecoration( - color: Colors.grey[400], - borderRadius: BorderRadius.circular(5), - ), - height: 135, - ), - ); - } - }, - ); - } -} - -class ThreadCardHeader extends StatelessWidget { - final Post p; - final Thread thread; - final bool small; - const ThreadCardHeader({ - Key? key, - required this.p, - required this.thread, - required this.small, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: p.member, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.hasError) { - return Row( - children: [], - ); - } - Member? m = snapshot.data as Member?; - if (m != null) { - Member? me = Provider.of(context); - bool owner = me != null && m.id == me.id; - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(5.0)), - child: Image.network( - m.image!, - width: small ? 40 : 50, - height: small ? 40 : 50, - errorBuilder: (BuildContext context, Object exception, - StackTrace? stackTrace) { - return Image.asset( - 'assets/noImage.png', - width: small ? 40 : 50, - height: small ? 40 : 50, - ); - }, - ), - ), - Padding( - padding: EdgeInsets.all(small ? 4.0 : 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - m.name, - style: TextStyle(fontSize: small ? 12 : 20), - ), - Text( - DateFormat('dd/MM/yyyy').format(thread.posted), - style: TextStyle(fontSize: small ? 10 : 14), - ) - ], - ), - ), - ], - ), - Row( - children: [ - // TODO: Implement edit and delete thread - // if (owner) - // Padding( - // padding: const EdgeInsets.all(8.0), - // child: IconButton( - // onPressed: () {}, icon: Icon(Icons.delete)), - // ), - // if (owner) - // Padding( - // padding: const EdgeInsets.all(8.0), - // child: IconButton( - // onPressed: () {}, icon: Icon(Icons.edit)), - // ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - thread.kind, - style: TextStyle(fontSize: small ? 12 : 16), - ), - ), - Container( - decoration: BoxDecoration( - color: THREADCOLOR[thread.status], - borderRadius: BorderRadius.circular(5)), - child: Padding( - padding: EdgeInsets.all(small ? 4.0 : 8.0), - child: Text( - thread.status, - style: TextStyle(fontSize: small ? 12 : 16), - ), - ), - ), - ], - ), - ], - ); - } else { - return Row( - children: [], - ); - } - } else { - return Row( - children: [], - ); - } - }, - ); - } -} - -class ThreadCardBody extends StatefulWidget { - Thread thread; - final Post post; - final bool small; - ThreadCardBody( - {Key? key, required this.thread, required this.post, required this.small}) - : super(key: key); - - @override - _ThreadCardBodyState createState() => _ThreadCardBodyState(); -} - -class _ThreadCardBodyState extends State { - bool _expanded = false; - late TextEditingController _newCommentController; - ThreadService _threadService = ThreadService(); - - @override - void initState() { - super.initState(); - _newCommentController = TextEditingController(); - } - - Widget _buildFooter(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: TextButton( - child: _expanded - ? Text('See less') - : Text(widget.thread.commentIds.length != 0 - ? widget.thread.commentIds.length == 1 - ? '${widget.thread.commentIds.length} comment' - : '${widget.thread.commentIds.length} comments' - : "Add comment"), - onPressed: () { - setState(() { - _expanded = !_expanded; - }); - }, - ), - ); - } - - Widget _buildComments(BuildContext context) { - return FutureBuilder( - future: widget.thread.comments, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.hasError) { - return Text('err'); - } - List comments = snapshot.data as List; - comments = comments.where((element) => element != null).toList(); - comments.sort((a, b) => a!.posted.compareTo(b!.posted)); - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...comments - .map((e) => Padding( - padding: const EdgeInsets.all(8.0), - child: CommentStrip(post: e!), - )) - .toList(), - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - controller: _newCommentController, - decoration: InputDecoration( - labelText: 'New comment', - disabledBorder: InputBorder.none, - suffixIcon: IconButton( - onPressed: () async { - String comment = _newCommentController.text; - if (comment.isNotEmpty) { - Thread? t = - await _threadService.addCommentToThread( - widget.thread.id, comment); - if (t != null) { - setState(() { - widget.thread = t; - }); - _newCommentController.clear(); - } - } - }, - icon: Icon(Icons.add_circle_outline_outlined), - ), - ), - ), - ), - ]); - } else { - return Shimmer.fromColors( - baseColor: Colors.grey[400]!, - highlightColor: Colors.white, - child: Column( - children: - widget.thread.commentIds.map((e) => Container()).toList(), - )); - } - }); - } - - @override - Widget build(BuildContext context) { - return AnimatedCrossFade( - firstChild: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.post.text ?? '', - style: TextStyle(fontSize: 16), - ), - Align( - alignment: Alignment.bottomRight, - child: _buildFooter(context), - ), - ], - ), - secondChild: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.post.text ?? '', - style: TextStyle(fontSize: 16), - ), - Padding( - padding: EdgeInsets.all(widget.small ? 4.0 : 8.0), - child: IntrinsicHeight( - child: Row( - children: [ - VerticalDivider( - color: Colors.grey, - width: 8, - thickness: 5, - ), - Expanded(child: _buildComments(context)), - ], - ), - ), - ), - Align( - alignment: Alignment.bottomRight, - child: _buildFooter(context), - ), - ], - ), - crossFadeState: - !_expanded ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: Duration(milliseconds: 250), - firstCurve: Curves.easeOut, - secondCurve: Curves.easeOut, - sizeCurve: Curves.easeOut, - ); - } -} - -class CommentStrip extends StatelessWidget { - final Post post; - const CommentStrip({Key? key, required this.post}) : super(key: key); - - Widget _buildHeader(BuildContext context) { - return FutureBuilder( - future: post.member, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.hasError) { - return Row( - children: [], - ); - } - Member? m = snapshot.data as Member?; - if (m != null) { - Member? me = Provider.of(context); - bool owner = me != null && m.id == me.id; - return IntrinsicWidth( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(5.0)), - child: Image.network( - m.image!, - width: 30, - height: 30, - errorBuilder: (BuildContext context, Object exception, - StackTrace? stackTrace) { - return Image.asset( - 'assets/noImage.png', - width: 30, - height: 30, - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - m.name, - style: TextStyle(fontSize: 18), - ), - Text( - DateFormat('dd/MM/yyyy').format(post.posted), - style: TextStyle(fontSize: 12), - ), - ], - ), - ), - if (owner) - Padding( - padding: const EdgeInsets.fromLTRB(4.0, 0, 0, 0), - child: IconButton( - onPressed: () {}, - icon: Icon(Icons.delete), - iconSize: 18, - ), - ), - if (owner) - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 4.0, 0), - child: IconButton( - onPressed: () {}, - icon: Icon(Icons.edit), - iconSize: 18, - ), - ), - ], - ), - ], - ), - ); - } else { - return Row( - children: [], - ); - } - } else { - return Row( - children: [], - ); - } - }, - ); - } - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Provider.of(context).isDark - ? Colors.grey[850] - : Colors.grey[300], - borderRadius: BorderRadius.circular(5), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(context), - SizedBox( - height: 16, - ), - Text( - post.text ?? '', - style: TextStyle(fontSize: 16), - ) - ], - ), - ), - ); - } -} diff --git a/frontend/lib/components/addThreadForm.dart b/frontend/lib/components/threads/addThreadForm.dart similarity index 94% rename from frontend/lib/components/addThreadForm.dart rename to frontend/lib/components/threads/addThreadForm.dart index 692f8656..deb9d0aa 100644 --- a/frontend/lib/components/addThreadForm.dart +++ b/frontend/lib/components/threads/addThreadForm.dart @@ -30,7 +30,7 @@ class AddThreadForm extends StatefulWidget { class _AddThreadFormState extends State { final _formKey = GlobalKey(); final _textController = TextEditingController(); - String kind = ''; + String kind = 'TEMPLATE'; void _submit(BuildContext context) async { if (_formKey.currentState!.validate()) { @@ -52,18 +52,22 @@ class _AddThreadFormState extends State { ), ); widget.onEditSpeaker!(context, s); + + Navigator.pop(context); } else { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('An error occured.')), ); + + Navigator.pop(context); } } else if (widget.company != null && widget.onEditCompany != null) { CompanyService service = CompanyService(); - Company? s = await service.addThread( - id: widget.speaker!.id, text: text, kind: kind); - if (s != null) { + Company? c = await service.addThread( + id: widget.company!.id, text: text, kind: kind); + if (c != null) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( @@ -72,13 +76,17 @@ class _AddThreadFormState extends State { duration: Duration(seconds: 2), ), ); - widget.onEditCompany!(context, s); + widget.onEditCompany!(context, c); + + Navigator.pop(context); } else { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('An error occured.')), ); + + Navigator.pop(context); } } else if (widget.meeting != null && widget.onEditMeeting != null) { MeetingService service = MeetingService(); diff --git a/frontend/lib/components/threads/commentStrip.dart b/frontend/lib/components/threads/commentStrip.dart new file mode 100644 index 00000000..2ae9a27a --- /dev/null +++ b/frontend/lib/components/threads/commentStrip.dart @@ -0,0 +1,222 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/components/blurryDialog.dart'; +import 'package:frontend/components/deckTheme.dart'; +import 'package:frontend/components/threads/editPostForm.dart'; +import 'package:frontend/models/member.dart'; +import 'package:frontend/models/post.dart'; +import 'package:frontend/models/thread.dart'; +import 'package:frontend/services/threadService.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class CommentStrip extends StatefulWidget { + Post post; + final String threadID; + final void Function(BuildContext, Thread?)? onEditThread; + + CommentStrip( + {Key? key, + required this.post, + required this.threadID, + required this.onEditThread}) + : super(key: key); + + @override + _CommentStripState createState() => _CommentStripState(); +} + +class _CommentStripState extends State { + ThreadService _threadService = ThreadService(); + + void _deleteCommentThread(context) async { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Deleting')), + ); + + Thread? t = await _threadService.deleteCommentFromThread( + widget.threadID, widget.post.id); + if (t != null) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + + widget.onEditThread!(context, t); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + } + } + + void _deleteCommentThreadDialog(context) { + showDialog( + context: context, + builder: (BuildContext context) { + return BlurryDialog('Warning', + 'Are you sure you want to delete comment with content ${widget.post.text}?', + () { + _deleteCommentThread(context); + }); + }, + ); + } + + Future postChangedCallback(BuildContext context, + {Future? fp, Post? post}) async { + Post? p; + if (fp != null) { + p = await fp; + } else if (post != null) { + p = post; + } + if (p != null) { + setState(() { + widget.post = p!; + }); + } + } + + void _editCommentModal(context) { + showModalBottomSheet( + context: context, + builder: (context) { + return Container( + child: EditPostForm( + post: widget.post, + onEditPost: (context, _post) { + postChangedCallback(context, post: _post); + })); + }, + ); + } + + Widget _buildHeader(BuildContext context) { + return FutureBuilder( + future: widget.post.member, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + return Row( + children: [], + ); + } + Member? m = snapshot.data as Member?; + if (m != null) { + Member? me = Provider.of(context); + bool owner = me != null && m.id == me.id; + return IntrinsicWidth( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + child: Image.network( + m.image!, + width: 30, + height: 30, + errorBuilder: (BuildContext context, Object exception, + StackTrace? stackTrace) { + return Image.asset( + 'assets/noImage.png', + width: 30, + height: 30, + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + m.name, + style: TextStyle(fontSize: 18), + ), + Text( + DateFormat('dd/MM/yyyy') + .format(widget.post.posted), + style: TextStyle(fontSize: 12), + ), + ], + ), + ), + if (owner) + Padding( + padding: const EdgeInsets.fromLTRB(4.0, 0, 0, 0), + child: IconButton( + onPressed: () { + _deleteCommentThreadDialog(context); + }, + icon: Icon(Icons.delete), + iconSize: 18, + ), + ), + if (owner) + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 4.0, 0), + child: IconButton( + onPressed: () { + _editCommentModal(context); + }, + icon: Icon(Icons.edit), + iconSize: 18, + ), + ), + ], + ), + ], + ), + ); + } else { + return Row( + children: [], + ); + } + } else { + return Row( + children: [], + ); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Provider.of(context).isDark + ? Colors.grey[850] + : Colors.grey[300], + borderRadius: BorderRadius.circular(5), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + SizedBox( + height: 16, + ), + Text( + widget.post.text ?? '', + style: TextStyle(fontSize: 16), + ) + ], + ), + ), + ); + } +} diff --git a/frontend/lib/components/threads/editPostForm.dart b/frontend/lib/components/threads/editPostForm.dart new file mode 100644 index 00000000..41506db5 --- /dev/null +++ b/frontend/lib/components/threads/editPostForm.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/models/post.dart'; +import 'package:frontend/services/postService.dart'; + +class EditPostForm extends StatefulWidget { + final Post post; + final void Function(BuildContext, Post?)? onEditPost; + + EditPostForm({Key? key, required this.post, required this.onEditPost}) + : super(key: key); + + @override + _EditPostFormState createState() => _EditPostFormState(); +} + +class _EditPostFormState extends State { + final _formKey = GlobalKey(); + late TextEditingController _textController; + + @override + void initState() { + super.initState(); + _textController = TextEditingController(text: widget.post.text); + } + + void _submit(BuildContext context) async { + if (_formKey.currentState!.validate()) { + var text = _textController.text; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Editing...')), + ); + + PostService _postService = PostService(); + Post? p = await _postService.updatePost(widget.post.id, text); + if (p != null) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Done'), + duration: Duration(seconds: 2), + ), + ); + widget.onEditPost!(context, p); + + Navigator.pop(context); + } else { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('An error occured.')), + ); + + Navigator.pop(context); + } + } + } + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.newline, + controller: _textController, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please put the contents of communication'; + } + return null; + }, + decoration: const InputDecoration( + icon: const Icon(Icons.work), + labelText: "Content *", + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: () => _submit(context), + child: const Text('Submit'), + ), + ), + ], + ), + ); + } +} diff --git a/frontend/lib/components/threads/participations/communicationsList.dart b/frontend/lib/components/threads/participations/communicationsList.dart new file mode 100644 index 00000000..b33fb789 --- /dev/null +++ b/frontend/lib/components/threads/participations/communicationsList.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/components/threads/participations/participationThreadsWidget.dart'; +import 'package:frontend/components/threads/threadCard.dart'; +import 'package:frontend/models/participation.dart'; + +class CommunicationsList extends StatelessWidget { + final List participations; + // ID of the meeting/company/speaker + final String id; + final CommunicationType type; + final bool small; + + CommunicationsList( + {Key? key, + required this.participations, + required this.small, + required this.type, + required this.id}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 20), + child: ListView( + controller: ScrollController(), + children: participations.reversed + .where((element) => + element.communicationsId != null && + element.communicationsId!.length != 0) + .map( + (participation) => ParticipationThreadsWidget( + participation: participation, + id: id, + type: type, + small: small, + ), + ) + .toList()), + ); + }); + } +} diff --git a/frontend/lib/components/threads/participations/participationThreadsWidget.dart b/frontend/lib/components/threads/participations/participationThreadsWidget.dart new file mode 100644 index 00000000..4044fec8 --- /dev/null +++ b/frontend/lib/components/threads/participations/participationThreadsWidget.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/components/threads/threadCard.dart'; +import 'package:frontend/models/participation.dart'; +import 'package:frontend/models/thread.dart'; + +class ParticipationThreadsWidget extends StatelessWidget { + final Participation participation; + // ID of the meeting/company/speaker + final String id; + final CommunicationType type; + final bool small; + + ParticipationThreadsWidget( + {Key? key, + required this.participation, + required this.small, + required this.type, + required this.id}) + : super(key: key); + + @override + Widget build(BuildContext context) { + print('Getting ${participation.event}'); + return Padding( + padding: const EdgeInsets.all(8.0), + child: FutureBuilder( + future: participation.communications, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Text('Error'); + } + if (snapshot.connectionState == ConnectionState.done) { + List? threads = snapshot.data as List?; + if (threads == null) { + threads = []; + } + threads.sort((a, b) => b.posted.compareTo(a.posted)); + return Column(children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text('SINFO ${participation.event}'), + ), + ), + Divider(), + ...threads + .map( + (thread) => Padding( + padding: const EdgeInsets.all(8.0), + child: ThreadCard( + thread: thread, + id: id, + type: type, + small: small, + ), + ), + ) + .toList(), + ]); + } else { + return Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } +} diff --git a/frontend/lib/components/threads/threadCard.dart b/frontend/lib/components/threads/threadCard.dart new file mode 100644 index 00000000..1ee342ab --- /dev/null +++ b/frontend/lib/components/threads/threadCard.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/components/threads/threadCardBody.dart'; +import 'package:frontend/components/threads/threadCardHeader.dart'; +import 'package:frontend/models/post.dart'; +import 'package:frontend/models/thread.dart'; +import 'package:shimmer/shimmer.dart'; + +final Map THREADCOLOR = { + "APPROVED": Colors.green, + "REVIEWED": Colors.green, + "PENDING": Colors.yellow, +}; + +enum CommunicationType { COMPANY, MEETING, SPEAKER } + +class ThreadCard extends StatefulWidget { + final Thread thread; + // ID of the meeting/company/speaker + final String id; + final CommunicationType type; + final bool small; + const ThreadCard( + {Key? key, + required this.thread, + required this.small, + required this.id, + required this.type}) + : super(key: key); + + @override + ThreadCardState createState() => ThreadCardState(); +} + +class ThreadCardState extends State + with AutomaticKeepAliveClientMixin { + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return FutureBuilder( + future: widget.thread.entry, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + return Text('Something went wrong'); + } + Post p = snapshot.data as Post; + return Padding( + padding: const EdgeInsets.all(8), + child: Container( + margin: EdgeInsets.fromLTRB(0, 20, 0, 0), + padding: EdgeInsets.fromLTRB(17, 15, 17, 15), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(5)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ThreadCardHeader( + p: p, + thread: widget.thread, + small: widget.small, + id: widget.id, + type: widget.type), + SizedBox( + height: 16, + ), + ThreadCardBody( + thread: widget.thread, + post: p, + small: widget.small, + ) + ], + ), + ), + ); + } else { + return Shimmer.fromColors( + baseColor: Colors.grey[400]!, + highlightColor: Colors.white, + child: Container( + margin: EdgeInsets.fromLTRB(0, 20, 0, 0), + padding: EdgeInsets.fromLTRB(17, 15, 17, 15), + decoration: BoxDecoration( + color: Colors.grey[400], + borderRadius: BorderRadius.circular(5), + ), + height: 135, + ), + ); + } + }, + ); + } +} diff --git a/frontend/lib/components/threads/threadCardBody.dart b/frontend/lib/components/threads/threadCardBody.dart new file mode 100644 index 00000000..3264f93c --- /dev/null +++ b/frontend/lib/components/threads/threadCardBody.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/components/threads/commentStrip.dart'; +import 'package:frontend/models/post.dart'; +import 'package:frontend/models/thread.dart'; +import 'package:frontend/services/threadService.dart'; +import 'package:shimmer/shimmer.dart'; + +class ThreadCardBody extends StatefulWidget { + Thread thread; + final Post post; + final bool small; + ThreadCardBody( + {Key? key, required this.thread, required this.post, required this.small}) + : super(key: key); + + @override + _ThreadCardBodyState createState() => _ThreadCardBodyState(); +} + +class _ThreadCardBodyState extends State { + bool _expanded = false; + late TextEditingController _newCommentController; + ThreadService _threadService = ThreadService(); + + @override + void initState() { + super.initState(); + _newCommentController = TextEditingController(); + } + + Widget _buildFooter(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: TextButton( + child: _expanded + ? Text('See less') + : Text(widget.thread.commentIds.length != 0 + ? widget.thread.commentIds.length == 1 + ? '${widget.thread.commentIds.length} comment' + : '${widget.thread.commentIds.length} comments' + : "Add comment"), + onPressed: () { + setState(() { + _expanded = !_expanded; + }); + }, + ), + ); + } + + Future threadChangedCallback(BuildContext context, + {Future? ft, Thread? thread}) async { + Thread? t; + if (ft != null) { + t = await ft; + } else if (thread != null) { + t = thread; + } + if (t != null) { + setState(() { + widget.thread = t!; + }); + } + } + + Widget _buildComments(BuildContext context) { + return FutureBuilder( + future: widget.thread.comments, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + return Text('err'); + } + List comments = snapshot.data as List; + comments = comments.where((element) => element != null).toList(); + comments.sort((a, b) => a!.posted.compareTo(b!.posted)); + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...comments + .map((e) => Padding( + padding: const EdgeInsets.all(8.0), + child: CommentStrip( + post: e!, + threadID: widget.thread.id, + onEditThread: (context, _thread) { + threadChangedCallback(context, thread: _thread); + }, + ), + )) + .toList(), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _newCommentController, + decoration: InputDecoration( + labelText: 'New comment', + disabledBorder: InputBorder.none, + suffixIcon: IconButton( + onPressed: () async { + String comment = _newCommentController.text; + if (comment.isNotEmpty) { + Thread? t = + await _threadService.addCommentToThread( + widget.thread.id, comment); + if (t != null) { + setState(() { + widget.thread = t; + }); + _newCommentController.clear(); + } + } + }, + icon: Icon(Icons.add_circle_outline_outlined), + ), + ), + ), + ), + ]); + } else { + return Shimmer.fromColors( + baseColor: Colors.grey[400]!, + highlightColor: Colors.white, + child: Column( + children: + widget.thread.commentIds.map((e) => Container()).toList(), + )); + } + }); + } + + @override + Widget build(BuildContext context) { + return AnimatedCrossFade( + firstChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.post.text ?? '', + style: TextStyle(fontSize: 16), + ), + Align( + alignment: Alignment.bottomRight, + child: _buildFooter(context), + ), + ], + ), + secondChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.post.text ?? '', + style: TextStyle(fontSize: 16), + ), + Padding( + padding: EdgeInsets.all(widget.small ? 4.0 : 8.0), + child: IntrinsicHeight( + child: Row( + children: [ + VerticalDivider( + color: Colors.grey, + width: 8, + thickness: 5, + ), + Expanded(child: _buildComments(context)), + ], + ), + ), + ), + Align( + alignment: Alignment.bottomRight, + child: _buildFooter(context), + ), + ], + ), + crossFadeState: + !_expanded ? CrossFadeState.showFirst : CrossFadeState.showSecond, + duration: Duration(milliseconds: 250), + firstCurve: Curves.easeOut, + secondCurve: Curves.easeOut, + sizeCurve: Curves.easeOut, + ); + } +} diff --git a/frontend/lib/components/threads/threadCardHeader.dart b/frontend/lib/components/threads/threadCardHeader.dart new file mode 100644 index 00000000..8daf56ee --- /dev/null +++ b/frontend/lib/components/threads/threadCardHeader.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/components/threads/threadCard.dart'; +import 'package:frontend/models/member.dart'; +import 'package:frontend/models/post.dart'; +import 'package:frontend/models/thread.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class ThreadCardHeader extends StatelessWidget { + final Post p; + final Thread thread; + // ID of the meeting/company/speaker + final String id; + final CommunicationType type; + final bool small; + const ThreadCardHeader( + {Key? key, + required this.p, + required this.thread, + required this.small, + required this.id, + required this.type}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: p.member, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + return Row( + children: [], + ); + } + Member? m = snapshot.data as Member?; + if (m != null) { + Member? me = Provider.of(context); + bool owner = me != null && m.id == me.id; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + child: Image.network( + m.image!, + width: small ? 40 : 50, + height: small ? 40 : 50, + errorBuilder: (BuildContext context, Object exception, + StackTrace? stackTrace) { + return Image.asset( + 'assets/noImage.png', + width: small ? 40 : 50, + height: small ? 40 : 50, + ); + }, + ), + ), + Padding( + padding: EdgeInsets.all(small ? 4.0 : 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + m.name, + style: TextStyle(fontSize: small ? 12 : 20), + ), + Text( + DateFormat('dd/MM/yyyy').format(thread.posted), + style: TextStyle(fontSize: small ? 10 : 14), + ) + ], + ), + ), + ], + ), + Row( + children: [ + if (owner) + Padding( + padding: const EdgeInsets.all(8.0), + child: IconButton( + onPressed: () { + print("Delete thread"); + }, + icon: Icon(Icons.delete)), + ), + if (owner) + Padding( + padding: const EdgeInsets.all(8.0), + child: IconButton( + onPressed: () { + print("Edit thread"); + }, + icon: Icon(Icons.edit)), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + thread.kind, + style: TextStyle(fontSize: small ? 12 : 16), + ), + ), + Container( + decoration: BoxDecoration( + color: THREADCOLOR[thread.status], + borderRadius: BorderRadius.circular(5)), + child: Padding( + padding: EdgeInsets.all(small ? 4.0 : 8.0), + child: Text( + thread.status, + style: TextStyle(fontSize: small ? 12 : 16), + ), + ), + ), + ], + ), + ], + ); + } else { + return Row( + children: [], + ); + } + } else { + return Row( + children: [], + ); + } + }, + ); + } +} diff --git a/frontend/lib/routes/company/CompanyScreen.dart b/frontend/lib/routes/company/CompanyScreen.dart index a9f53863..c4b9d553 100644 --- a/frontend/lib/routes/company/CompanyScreen.dart +++ b/frontend/lib/routes/company/CompanyScreen.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:frontend/components/EditableCard.dart'; -import 'package:frontend/components/addThreadForm.dart'; +import 'package:frontend/components/threads/addThreadForm.dart'; import 'package:frontend/components/appbar.dart'; import 'package:frontend/components/deckTheme.dart'; import 'package:frontend/components/eventNotifier.dart'; import 'package:frontend/components/participationCard.dart'; -import 'package:frontend/components/threadCard.dart'; +import 'package:frontend/components/threads/participations/communicationsList.dart'; +import 'package:frontend/components/threads/threadCard.dart'; import 'package:frontend/models/company.dart'; import 'package:frontend/routes/company/CompanyTableNotifier.dart'; import 'package:frontend/routes/company/EditCompanyForm.dart'; @@ -181,6 +182,8 @@ class _CompanyScreenState extends State ), CommunicationsList( participations: widget.company.participations ?? [], + id: widget.company.id, + type: CommunicationType.COMPANY, small: small), ]), ), diff --git a/frontend/lib/routes/meeting/AddMeetingMemberForm.dart b/frontend/lib/routes/meeting/AddMeetingMemberForm.dart index 32517b3e..f740c850 100644 --- a/frontend/lib/routes/meeting/AddMeetingMemberForm.dart +++ b/frontend/lib/routes/meeting/AddMeetingMemberForm.dart @@ -86,7 +86,6 @@ class _AddMeetingMemberForm extends State { builder: (context, snapshot) { if (snapshot.hasData) { List membsMatched = snapshot.data as List; - print("membs matched:" + membsMatched.toString()); return searchResults(membsMatched, height); } else { return Center(child: CircularProgressIndicator()); diff --git a/frontend/lib/routes/meeting/MeetingScreen.dart b/frontend/lib/routes/meeting/MeetingScreen.dart index ce31d59c..249f8f64 100644 --- a/frontend/lib/routes/meeting/MeetingScreen.dart +++ b/frontend/lib/routes/meeting/MeetingScreen.dart @@ -1,12 +1,12 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:frontend/components/ListViewCard.dart'; -import 'package:frontend/components/addThreadForm.dart'; +import 'package:frontend/components/threads/addThreadForm.dart'; import 'package:frontend/components/appbar.dart'; import 'package:frontend/components/blurryDialog.dart'; import 'package:frontend/components/deckTheme.dart'; import 'package:frontend/components/eventNotifier.dart'; -import 'package:frontend/components/threadCard.dart'; +import 'package:frontend/components/threads/threadCard.dart'; import 'package:frontend/models/meeting.dart'; import 'package:frontend/models/member.dart'; import 'package:frontend/models/thread.dart'; @@ -156,7 +156,8 @@ class _MeetingScreenState extends State }), MeetingsCommunications( communications: widget.meeting.communications, - small: small), + small: small, + id: widget.meeting.id), ]), ), ], @@ -317,9 +318,13 @@ class MeetingParticipants extends StatelessWidget { class MeetingsCommunications extends StatelessWidget { final Future?> communications; final bool small; + final String id; MeetingsCommunications( - {Key? key, required this.communications, required this.small}); + {Key? key, + required this.communications, + required this.small, + required this.id}); @override Widget build(BuildContext context) { @@ -344,6 +349,8 @@ class MeetingsCommunications extends StatelessWidget { padding: const EdgeInsets.all(8.0), child: ThreadCard( thread: thread, + type: CommunicationType.MEETING, + id: id, small: small, ), ), diff --git a/frontend/lib/routes/speaker/SpeakerScreen.dart b/frontend/lib/routes/speaker/SpeakerScreen.dart index e2929432..13276c87 100644 --- a/frontend/lib/routes/speaker/SpeakerScreen.dart +++ b/frontend/lib/routes/speaker/SpeakerScreen.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:frontend/components/EditableCard.dart'; -import 'package:frontend/components/addThreadForm.dart'; +import 'package:frontend/components/threads/addThreadForm.dart'; import 'package:frontend/components/appbar.dart'; import 'package:frontend/components/deckTheme.dart'; import 'package:frontend/components/eventNotifier.dart'; import 'package:frontend/components/participationCard.dart'; import 'package:frontend/components/router.dart'; -import 'package:frontend/components/threadCard.dart'; +import 'package:frontend/components/threads/participations/communicationsList.dart'; +import 'package:frontend/components/threads/threadCard.dart'; import 'package:frontend/routes/speaker/speakerNotifier.dart'; import 'package:frontend/components/status.dart'; import 'package:frontend/main.dart'; @@ -125,6 +126,8 @@ class _SpeakerScreenState extends State ), CommunicationsList( participations: widget.speaker.participations ?? [], + id: widget.speaker.id, + type: CommunicationType.SPEAKER, small: small), ]), ), diff --git a/frontend/lib/routes/teams/AddTeamMemberForm.dart b/frontend/lib/routes/teams/AddTeamMemberForm.dart index 35c5e0f3..d0b6a521 100644 --- a/frontend/lib/routes/teams/AddTeamMemberForm.dart +++ b/frontend/lib/routes/teams/AddTeamMemberForm.dart @@ -144,7 +144,6 @@ class _AddTeamMemberFormState extends State { builder: (context, snapshot) { if (snapshot.hasData) { List membsMatched = snapshot.data as List; - print("membs matched:" + membsMatched.toString()); return searchResults(membsMatched, height); } else { return Center(child: CircularProgressIndicator()); diff --git a/frontend/lib/services/companyService.dart b/frontend/lib/services/companyService.dart index 430adef4..0e48bf0f 100644 --- a/frontend/lib/services/companyService.dart +++ b/frontend/lib/services/companyService.dart @@ -424,4 +424,19 @@ class CompanyService extends Service { throw DeckException('Wrong format'); } } + + Future deleteThread( + {required String id, required String threadID}) async { + Response response = await dio + .delete("/companies/" + id + "/participation/thread/" + threadID); + try { + return Company.fromJson(json.decode(response.data!)); + } on SocketException { + throw DeckException('No Internet connection'); + } on HttpException { + throw DeckException('Not found'); + } on FormatException { + throw DeckException('Wrong format'); + } + } } diff --git a/frontend/lib/services/meetingService.dart b/frontend/lib/services/meetingService.dart index 6cb9de02..ccc03aea 100644 --- a/frontend/lib/services/meetingService.dart +++ b/frontend/lib/services/meetingService.dart @@ -207,4 +207,19 @@ class MeetingService extends Service { throw DeckException('Wrong format'); } } + + Future deleteThread( + {required String id, required String threadID}) async { + Response response = + await dio.delete('/meetings/' + id + '/thread/' + threadID); + try { + return Meeting.fromJson(json.decode(response.data!)); + } on SocketException { + throw DeckException('No Internet connection'); + } on HttpException { + throw DeckException('Not found'); + } on FormatException { + throw DeckException('Wrong format'); + } + } } diff --git a/frontend/lib/services/speakerService.dart b/frontend/lib/services/speakerService.dart index e3b5b1a6..7d423d0a 100644 --- a/frontend/lib/services/speakerService.dart +++ b/frontend/lib/services/speakerService.dart @@ -123,7 +123,6 @@ class SpeakerService extends Service { String? notes, String? title}) async { var body = {"bio": bio, "name": name, "notes": notes, "title": title}; - print(body); try { Response response = await dio.put("/speakers/" + id, data: body); @@ -430,4 +429,19 @@ class SpeakerService extends Service { throw DeckException('Wrong format'); } } + + Future deleteThread( + {required String id, required String threadID}) async { + Response response = await dio + .delete("/speakers/" + id + "/participation/thread/" + threadID); + try { + return Speaker.fromJson(json.decode(response.data!)); + } on SocketException { + throw DeckException('No Internet connection'); + } on HttpException { + throw DeckException('Not found'); + } on FormatException { + throw DeckException('Wrong format'); + } + } }