fix: post creation, reaction like

This commit is contained in:
mike doornenbal 2024-07-31 16:40:12 +02:00
parent eb953ede0d
commit a8897242e7
11 changed files with 505 additions and 280 deletions

View file

@ -405,4 +405,81 @@ class FirebaseTimelinePostService
notifyListeners(); notifyListeners();
return categories; return categories;
} }
@override
Future<TimelinePost> likeReaction(
String userId,
TimelinePost post,
String reactionId,
) async {
// update the post with the new like
var updatedPost = post.copyWith(
reactions: post.reactions?.map(
(r) {
if (r.id == reactionId) {
return r.copyWith(
likedBy: (r.likedBy ?? [])..add(userId),
);
}
return r;
},
).toList(),
);
posts = posts
.map(
(p) => p.id == post.id ? updatedPost : p,
)
.toList();
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
await postRef.update({
'reactions': post.reactions
?.map(
(r) =>
r.id == reactionId ? r.copyWith(likedBy: r.likedBy ?? []) : r,
)
.map((e) => e.toJson())
.toList(),
});
notifyListeners();
return updatedPost;
}
@override
Future<TimelinePost> unlikeReaction(
String userId,
TimelinePost post,
String reactionId,
) async {
// update the post with the new like
var updatedPost = post.copyWith(
reactions: post.reactions?.map(
(r) {
if (r.id == reactionId) {
return r.copyWith(
likedBy: r.likedBy?..remove(userId),
);
}
return r;
},
).toList(),
);
posts = posts
.map(
(p) => p.id == post.id ? updatedPost : p,
)
.toList();
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
await postRef.update({
'reactions': post.reactions
?.map(
(r) => r.id == reactionId
? r.copyWith(likedBy: r.likedBy?..remove(userId))
: r,
)
.map((e) => e.toJson())
.toList(),
});
notifyListeners();
return updatedPost;
}
} }

View file

@ -16,6 +16,7 @@ class TimelinePostReaction {
this.imageUrl, this.imageUrl,
this.creator, this.creator,
this.createdAtString, this.createdAtString,
this.likedBy,
}); });
factory TimelinePostReaction.fromJson( factory TimelinePostReaction.fromJson(
@ -31,6 +32,7 @@ class TimelinePostReaction {
imageUrl: json['image_url'] as String?, imageUrl: json['image_url'] as String?,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
createdAtString: json['created_at'] as String, createdAtString: json['created_at'] as String,
likedBy: (json['liked_by'] as List<dynamic>?)?.cast<String>() ?? [],
); );
/// The unique identifier of the reaction. /// The unique identifier of the reaction.
@ -57,6 +59,8 @@ class TimelinePostReaction {
/// Reaction creation date as String with microseconds. /// Reaction creation date as String with microseconds.
final String? createdAtString; final String? createdAtString;
final List<String>? likedBy;
TimelinePostReaction copyWith({ TimelinePostReaction copyWith({
String? id, String? id,
String? postId, String? postId,
@ -65,6 +69,7 @@ class TimelinePostReaction {
String? reaction, String? reaction,
String? imageUrl, String? imageUrl,
DateTime? createdAt, DateTime? createdAt,
List<String>? likedBy,
}) => }) =>
TimelinePostReaction( TimelinePostReaction(
id: id ?? this.id, id: id ?? this.id,
@ -74,6 +79,7 @@ class TimelinePostReaction {
reaction: reaction ?? this.reaction, reaction: reaction ?? this.reaction,
imageUrl: imageUrl ?? this.imageUrl, imageUrl: imageUrl ?? this.imageUrl,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
likedBy: likedBy ?? this.likedBy,
); );
Map<String, dynamic> toJson() => <String, dynamic>{ Map<String, dynamic> toJson() => <String, dynamic>{
@ -82,6 +88,7 @@ class TimelinePostReaction {
'reaction': reaction, 'reaction': reaction,
'image_url': imageUrl, 'image_url': imageUrl,
'created_at': createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
'liked_by': likedBy,
}, },
}; };
@ -91,6 +98,7 @@ class TimelinePostReaction {
'reaction': reaction, 'reaction': reaction,
'image_url': imageUrl, 'image_url': imageUrl,
'created_at': createdAtString, 'created_at': createdAtString,
'liked_by': likedBy,
}, },
}; };
} }

View file

@ -32,4 +32,14 @@ abstract class TimelinePostService with ChangeNotifier {
Future<List<TimelineCategory>> fetchCategories(); Future<List<TimelineCategory>> fetchCategories();
Future<bool> addCategory(TimelineCategory category); Future<bool> addCategory(TimelineCategory category);
Future<TimelinePost> likeReaction(
String userId,
TimelinePost post,
String reactionId,
);
Future<TimelinePost> unlikeReaction(
String userId,
TimelinePost post,
String reactionId,
);
} }

View file

@ -54,6 +54,9 @@ class TimelineTranslations {
required this.addCategorySubmitButton, required this.addCategorySubmitButton,
required this.addCategoryCancelButtton, required this.addCategoryCancelButtton,
required this.addCategoryHintText, required this.addCategoryHintText,
required this.addCategoryErrorText,
required this.titleErrorText,
required this.contentErrorText,
}); });
/// Default translations for the timeline component view /// Default translations for the timeline component view
@ -100,6 +103,9 @@ class TimelineTranslations {
this.addCategorySubmitButton = 'Add category', this.addCategorySubmitButton = 'Add category',
this.addCategoryCancelButtton = 'Cancel', this.addCategoryCancelButtton = 'Cancel',
this.addCategoryHintText = 'Category name...', this.addCategoryHintText = 'Category name...',
this.addCategoryErrorText = 'Please enter a category name',
this.titleErrorText = 'Please enter a title',
this.contentErrorText = 'Please enter content',
}); });
final String noPosts; final String noPosts;
@ -117,6 +123,8 @@ class TimelineTranslations {
final String titleHintText; final String titleHintText;
final String contentHintText; final String contentHintText;
final String titleErrorText;
final String contentErrorText;
final String deletePost; final String deletePost;
final String deleteConfirmationTitle; final String deleteConfirmationTitle;
@ -147,6 +155,7 @@ class TimelineTranslations {
final String addCategorySubmitButton; final String addCategorySubmitButton;
final String addCategoryCancelButtton; final String addCategoryCancelButtton;
final String addCategoryHintText; final String addCategoryHintText;
final String addCategoryErrorText;
final String yes; final String yes;
final String no; final String no;
@ -194,6 +203,9 @@ class TimelineTranslations {
String? addCategorySubmitButton, String? addCategorySubmitButton,
String? addCategoryCancelButtton, String? addCategoryCancelButtton,
String? addCategoryHintText, String? addCategoryHintText,
String? addCategoryErrorText,
String? titleErrorText,
String? contentErrorText,
}) => }) =>
TimelineTranslations( TimelineTranslations(
noPosts: noPosts ?? this.noPosts, noPosts: noPosts ?? this.noPosts,
@ -244,5 +256,8 @@ class TimelineTranslations {
addCategoryHintText: addCategoryHintText ?? this.addCategoryHintText, addCategoryHintText: addCategoryHintText ?? this.addCategoryHintText,
createCategoryPopuptitle: createCategoryPopuptitle:
createCategoryPopuptitle ?? this.createCategoryPopuptitle, createCategoryPopuptitle ?? this.createCategoryPopuptitle,
addCategoryErrorText: addCategoryErrorText ?? this.addCategoryErrorText,
titleErrorText: titleErrorText ?? this.titleErrorText,
contentErrorText: contentErrorText ?? this.contentErrorText,
); );
} }

View file

@ -53,48 +53,23 @@ class _TimelinePostCreationScreenState
TextEditingController titleController = TextEditingController(); TextEditingController titleController = TextEditingController();
TextEditingController contentController = TextEditingController(); TextEditingController contentController = TextEditingController();
Uint8List? image; Uint8List? image;
bool editingDone = false;
bool allowComments = false; bool allowComments = false;
bool titleIsValid = false;
bool contentIsValid = false;
@override @override
void initState() { void initState() {
titleController.addListener(_listenForInputs);
contentController.addListener(_listenForInputs);
super.initState(); super.initState();
titleController.addListener(checkIfEditingDone);
contentController.addListener(checkIfEditingDone);
} }
@override void _listenForInputs() {
void dispose() { titleIsValid = titleController.text.isNotEmpty;
titleController.dispose(); contentIsValid = contentController.text.isNotEmpty;
contentController.dispose();
super.dispose();
} }
void checkIfEditingDone() { var formkey = GlobalKey<FormState>();
setState(() {
editingDone =
titleController.text.isNotEmpty && contentController.text.isNotEmpty;
if (widget.options.requireImageForPost) {
editingDone = editingDone && image != null;
}
if (widget.options.minTitleLength != null) {
editingDone = editingDone &&
titleController.text.length >= widget.options.minTitleLength!;
}
if (widget.options.maxTitleLength != null) {
editingDone = editingDone &&
titleController.text.length <= widget.options.maxTitleLength!;
}
if (widget.options.minContentLength != null) {
editingDone = editingDone &&
contentController.text.length >= widget.options.minContentLength!;
}
if (widget.options.maxContentLength != null) {
editingDone = editingDone &&
contentController.text.length <= widget.options.maxContentLength!;
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -127,251 +102,274 @@ class _TimelinePostCreationScreenState
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
padding: widget.options.paddings.mainPadding, padding: widget.options.paddings.mainPadding,
child: Column( child: Form(
mainAxisSize: MainAxisSize.max, key: formkey,
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ mainAxisSize: MainAxisSize.max,
Text( crossAxisAlignment: CrossAxisAlignment.start,
widget.options.translations.title, children: [
style: theme.textTheme.titleMedium, Text(
), widget.options.translations.title,
const SizedBox( style: theme.textTheme.titleMedium,
height: 4, ),
), const SizedBox(
widget.options.textInputBuilder?.call( height: 4,
titleController, ),
null, widget.options.textInputBuilder?.call(
'', titleController,
) ?? null,
PostCreationTextfield( '',
controller: titleController, ) ??
hintText: widget.options.translations.titleHintText, PostCreationTextfield(
textMaxLength: widget.options.maxTitleLength, controller: titleController,
decoration: widget.options.titleInputDecoration, hintText: widget.options.translations.titleHintText,
textCapitalization: TextCapitalization.sentences, textMaxLength: widget.options.maxTitleLength,
expands: null, decoration: widget.options.titleInputDecoration,
minLines: null, textCapitalization: TextCapitalization.sentences,
maxLines: 1, expands: null,
), minLines: null,
const SizedBox(height: 16), maxLines: 1,
Text( validator: (value) {
widget.options.translations.content, if (value == null || value.isEmpty) {
style: theme.textTheme.titleMedium, return widget.options.translations.titleErrorText;
), }
Text( if (value.trim().isEmpty) {
widget.options.translations.contentDescription, return widget.options.translations.titleErrorText;
style: theme.textTheme.bodySmall, }
), return null;
const SizedBox( },
height: 4, ),
), const SizedBox(height: 16),
PostCreationTextfield( Text(
controller: contentController, widget.options.translations.content,
hintText: widget.options.translations.contentHintText, style: theme.textTheme.titleMedium,
textMaxLength: null, ),
decoration: widget.options.contentInputDecoration, Text(
textCapitalization: TextCapitalization.sentences, widget.options.translations.contentDescription,
expands: false, style: theme.textTheme.bodySmall,
minLines: null, ),
maxLines: null, const SizedBox(
), height: 4,
const SizedBox( ),
height: 16, PostCreationTextfield(
), controller: contentController,
Text( hintText: widget.options.translations.contentHintText,
widget.options.translations.uploadImage, textMaxLength: null,
style: theme.textTheme.titleMedium, decoration: widget.options.contentInputDecoration,
), textCapitalization: TextCapitalization.sentences,
Text( expands: false,
widget.options.translations.uploadImageDescription, minLines: null,
style: theme.textTheme.bodySmall, maxLines: null,
), validator: (value) {
const SizedBox( if (value == null || value.isEmpty) {
height: 8, return widget.options.translations.contentErrorText;
), }
Stack( if (value.trim().isEmpty) {
children: [ return widget.options.translations.contentErrorText;
GestureDetector( }
onTap: () async { return null;
var result = await showModalBottomSheet<Uint8List?>( },
context: context, ),
builder: (context) => Container( const SizedBox(
padding: const EdgeInsets.all(8.0), height: 16,
color: theme.colorScheme.surface, ),
child: ImagePicker( Text(
imagePickerConfig: widget.options.imagePickerConfig, widget.options.translations.uploadImage,
imagePickerTheme: widget.options.imagePickerTheme ?? style: theme.textTheme.titleMedium,
ImagePickerTheme( ),
titleAlignment: TextAlign.center, Text(
title: ' Do you want to upload a file' widget.options.translations.uploadImageDescription,
' or take a picture? ', style: theme.textTheme.bodySmall,
titleTextSize: ),
theme.textTheme.titleMedium!.fontSize!, const SizedBox(
font: height: 8,
theme.textTheme.titleMedium!.fontFamily!, ),
iconSize: 40, Stack(
selectImageText: 'UPLOAD FILE', children: [
makePhotoText: 'TAKE PICTURE', GestureDetector(
selectImageIcon: const Icon( onTap: () async {
size: 40, var result = await showModalBottomSheet<Uint8List?>(
Icons.insert_drive_file, context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(8.0),
color: theme.colorScheme.surface,
child: ImagePicker(
imagePickerConfig:
widget.options.imagePickerConfig,
imagePickerTheme: widget
.options.imagePickerTheme ??
ImagePickerTheme(
titleAlignment: TextAlign.center,
title: ' Do you want to upload a file'
' or take a picture? ',
titleTextSize:
theme.textTheme.titleMedium!.fontSize!,
font: theme
.textTheme.titleMedium!.fontFamily!,
iconSize: 40,
selectImageText: 'UPLOAD FILE',
makePhotoText: 'TAKE PICTURE',
selectImageIcon: const Icon(
size: 40,
Icons.insert_drive_file,
),
),
customButton: TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(
'Cancel',
style: theme.textTheme.bodyMedium!.copyWith(
decoration: TextDecoration.underline,
), ),
), ),
customButton: TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(
'Cancel',
style: theme.textTheme.bodyMedium!.copyWith(
decoration: TextDecoration.underline,
),
), ),
), ),
), ),
), );
); if (result != null) {
if (result != null) { setState(() {
setState(() { image = result;
image = result; });
}); }
} },
checkIfEditingDone(); child: ClipRRect(
}, borderRadius: BorderRadius.circular(8.0),
child: ClipRRect( child: image != null
borderRadius: BorderRadius.circular(8.0), ? Image.memory(
child: image != null image!,
? Image.memory(
image!,
width: double.infinity,
height: 150.0,
fit: BoxFit.cover,
// give it a rounded border
)
: DottedBorder(
dashPattern: const [4, 4],
radius: const Radius.circular(8.0),
color: theme.textTheme.displayMedium?.color ??
Colors.white,
child: const SizedBox(
width: double.infinity, width: double.infinity,
height: 150.0, height: 150.0,
child: Icon( fit: BoxFit.cover,
Icons.image, // give it a rounded border
size: 50, )
: DottedBorder(
dashPattern: const [4, 4],
radius: const Radius.circular(8.0),
color: theme.textTheme.displayMedium?.color ??
Colors.white,
child: const SizedBox(
width: double.infinity,
height: 150.0,
child: Icon(
Icons.image,
size: 50,
),
), ),
), ),
), ),
), ),
), // if an image is selected, show a delete button
// if an image is selected, show a delete button if (image != null) ...[
if (image != null) ...[ Positioned(
Positioned( top: 8,
top: 8, right: 8,
right: 8, child: GestureDetector(
child: GestureDetector( onTap: () {
onTap: () { setState(() {
setState(() { image = null;
image = null; });
}); },
checkIfEditingDone(); child: Container(
}, decoration: BoxDecoration(
child: Container( color: Colors.black.withOpacity(0.5),
decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.0),
color: Colors.black.withOpacity(0.5), ),
borderRadius: BorderRadius.circular(8.0), child: const Icon(
), Icons.delete,
child: const Icon( color: Colors.white,
Icons.delete, ),
color: Colors.white,
), ),
), ),
), ),
],
],
),
const SizedBox(height: 16),
Text(
widget.options.translations.commentsTitle,
style: theme.textTheme.titleMedium,
),
Text(
widget.options.translations.allowCommentsDescription,
style: theme.textTheme.bodySmall,
),
const SizedBox(
height: 8,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
activeColor: theme.colorScheme.primary,
value: allowComments,
onChanged: (value) {
setState(() {
allowComments = true;
});
},
),
Text(
widget.options.translations.yes,
style: theme.textTheme.bodyMedium,
),
const SizedBox(
width: 32,
),
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
activeColor: theme.colorScheme.primary,
value: !allowComments,
onChanged: (value) {
setState(() {
allowComments = false;
});
},
),
Text(
widget.options.translations.no,
style: theme.textTheme.bodyMedium,
), ),
], ],
],
),
const SizedBox(height: 16),
Text(
widget.options.translations.commentsTitle,
style: theme.textTheme.titleMedium,
),
Text(
widget.options.translations.allowCommentsDescription,
style: theme.textTheme.bodySmall,
),
const SizedBox(
height: 8,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
activeColor: theme.colorScheme.primary,
value: allowComments,
onChanged: (value) {
setState(() {
allowComments = true;
});
},
),
Text(
widget.options.translations.yes,
style: theme.textTheme.bodyMedium,
),
const SizedBox(
width: 32,
),
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
activeColor: theme.colorScheme.primary,
value: !allowComments,
onChanged: (value) {
setState(() {
allowComments = false;
});
},
),
Text(
widget.options.translations.no,
style: theme.textTheme.bodyMedium,
),
],
),
const SizedBox(height: 120),
SafeArea(
bottom: true,
child: Align(
alignment: Alignment.bottomCenter,
child: widget.options.buttonBuilder?.call(
context,
onPostCreated,
widget.options.translations.checkPost,
enabled: editingDone,
) ??
Padding(
padding: const EdgeInsets.symmetric(horizontal: 48),
child: DefaultFilledButton(
onPressed: editingDone
? () async {
await onPostCreated();
await widget.service.postService
.fetchPosts(null);
}
: null,
buttonText: widget.enablePostOverviewScreen
? widget.options.translations.checkPost
: widget.options.translations.postCreation,
),
),
), ),
), const SizedBox(height: 120),
], SafeArea(
bottom: true,
child: Align(
alignment: Alignment.bottomCenter,
child: widget.options.buttonBuilder?.call(
context,
onPostCreated,
widget.options.translations.checkPost,
enabled: formkey.currentState!.validate(),
) ??
Padding(
padding: const EdgeInsets.symmetric(horizontal: 48),
child: DefaultFilledButton(
onPressed: titleIsValid && contentIsValid
? () async {
if (formkey.currentState!.validate()) {
await onPostCreated();
await widget.service.postService
.fetchPosts(null);
}
}
: null,
buttonText: widget.enablePostOverviewScreen
? widget.options.translations.checkPost
: widget.options.translations.postCreation,
),
),
),
),
],
),
), ),
), ),
), ),

View file

@ -344,13 +344,19 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
setState(() {}); setState(() {});
} }
}, },
icon: Icon( icon: isLikedByUser
isLikedByUser ? widget.options.theme.likedIcon ??
? Icons.favorite_rounded Icon(
: Icons.favorite_outline_outlined, Icons.favorite_rounded,
color: widget.options.theme.iconColor, color: widget.options.theme.iconColor,
size: widget.options.iconSize, size: widget.options.iconSize,
), )
: widget.options.theme.likeIcon ??
Icon(
Icons.favorite_outline_outlined,
color: widget.options.theme.iconColor,
size: widget.options.iconSize,
),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
if (post.reactionEnabled) if (post.reactionEnabled)
@ -410,7 +416,7 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// ignore: avoid_bool_literals_in_conditional_expressions // ignore: avoid_bool_literals_in_conditional_expressions
if (post.reactionEnabled || widget.isOverviewScreen != null if (post.reactionEnabled && widget.isOverviewScreen != null
? !widget.isOverviewScreen! ? !widget.isOverviewScreen!
: false) ...[ : false) ...[
Text( Text(
@ -544,6 +550,55 @@ class _TimelinePostScreenState extends State<TimelinePostScreen> {
), ),
), ),
], ],
Builder(
builder: (context) {
var isLikedByUser =
reaction.likedBy?.contains(widget.userId) ??
false;
return IconButton(
padding: const EdgeInsets.only(left: 12),
constraints: const BoxConstraints(),
onPressed: () async {
if (isLikedByUser) {
updatePost(
await widget.service.postService
.unlikeReaction(
widget.userId,
post,
reaction.id,
),
);
setState(() {});
} else {
updatePost(
await widget.service.postService
.likeReaction(
widget.userId,
post,
reaction.id,
),
);
setState(() {});
}
},
icon: isLikedByUser
? widget.options.theme.likedIcon ??
Icon(
Icons.favorite_rounded,
color:
widget.options.theme.iconColor,
size: 14,
)
: widget.options.theme.likeIcon ??
Icon(
Icons.favorite_outline_outlined,
color:
widget.options.theme.iconColor,
size: 14,
),
);
},
),
], ],
), ),
), ),

View file

@ -163,6 +163,9 @@ class _TimelineSelectionScreenState extends State<TimelineSelectionScreen> {
PostCreationTextfield( PostCreationTextfield(
controller: controller, controller: controller,
hintText: widget.options.translations.addCategoryHintText, hintText: widget.options.translations.addCategoryHintText,
validator: (p0) => p0!.isEmpty
? widget.options.translations.addCategoryErrorText
: null,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
@ -180,7 +183,7 @@ class _TimelineSelectionScreenState extends State<TimelineSelectionScreen> {
), ),
); );
setState(() {}); setState(() {});
if (mounted) Navigator.pop(context); if (context.mounted) Navigator.pop(context);
}, },
buttonText: buttonText:
widget.options.translations.addCategorySubmitButton, widget.options.translations.addCategorySubmitButton,

View file

@ -272,4 +272,60 @@ class LocalTimelinePostService
return categories; return categories;
} }
@override
Future<TimelinePost> likeReaction(
String userId,
TimelinePost post,
String reactionId,
) async {
var updatedPost = post.copyWith(
reactions: post.reactions?.map(
(r) {
if (r.id == reactionId) {
return r.copyWith(
likedBy: (r.likedBy ?? [])..add(userId),
);
}
return r;
},
).toList(),
);
posts = posts
.map(
(p) => p.id == post.id ? updatedPost : p,
)
.toList();
notifyListeners();
return updatedPost;
}
@override
Future<TimelinePost> unlikeReaction(
String userId,
TimelinePost post,
String reactionId,
) async {
var updatedPost = post.copyWith(
reactions: post.reactions?.map(
(r) {
if (r.id == reactionId) {
return r.copyWith(
likedBy: r.likedBy?..remove(userId),
);
}
return r;
},
).toList(),
);
posts = posts
.map(
(p) => p.id == post.id ? updatedPost : p,
)
.toList();
notifyListeners();
return updatedPost;
}
} }

View file

@ -14,11 +14,13 @@ class DefaultFilledButton extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
return FilledButton( return FilledButton(
style: ButtonStyle( style: onPressed != null
backgroundColor: WidgetStatePropertyAll( ? ButtonStyle(
theme.colorScheme.primary, backgroundColor: WidgetStatePropertyAll(
), theme.colorScheme.primary,
), ),
)
: null,
onPressed: onPressed, onPressed: onPressed,
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),

View file

@ -4,6 +4,7 @@ class PostCreationTextfield extends StatelessWidget {
const PostCreationTextfield({ const PostCreationTextfield({
required this.controller, required this.controller,
required this.hintText, required this.hintText,
required this.validator,
super.key, super.key,
this.textMaxLength, this.textMaxLength,
this.decoration, this.decoration,
@ -22,10 +23,12 @@ class PostCreationTextfield extends StatelessWidget {
final bool? expands; final bool? expands;
final int? minLines; final int? minLines;
final int? maxLines; final int? maxLines;
final String? Function(String?)? validator;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
return TextField( return TextFormField(
validator: validator,
style: theme.textTheme.bodySmall, style: theme.textTheme.bodySmall,
controller: controller, controller: controller,
maxLength: textMaxLength, maxLength: textMaxLength,

View file

@ -315,9 +315,7 @@ class _TimelinePostWidgetState extends State<TimelinePostWidget> {
] else ...[ ] else ...[
Text( Text(
'${widget.post.likes} ' '${widget.post.likes} '
'${widget.post.likes > 1 ? '${widget.post.likes > 1 ? widget.options.translations.multipleLikesTitle : widget.options.translations.oneLikeTitle}',
widget.options.translations.multipleLikesTitle :
widget.options.translations.oneLikeTitle}',
style: style:
widget.options.theme.textStyles.listPostLikeTitleAndAmount ?? widget.options.theme.textStyles.listPostLikeTitleAndAmount ??
theme.textTheme.titleSmall!.copyWith( theme.textTheme.titleSmall!.copyWith(