feat: add firebase implementation of timeline service

This commit is contained in:
Freek van de Ven 2023-11-20 08:20:37 +01:00
parent d24731412f
commit 4113e9fea2
10 changed files with 366 additions and 3 deletions

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
@immutable
class FirebaseTimelineOptions {
const FirebaseTimelineOptions({
this.usersCollectionName = 'users',
this.timelineCollectionName = 'timeline',
this.allTimelineCategories = const [],
});
final String usersCollectionName;
final String timelineCollectionName;
final List<String> allTimelineCategories;
}

View file

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
@immutable
class FirebaseUserDocument {
const FirebaseUserDocument({
this.firstName,
this.lastName,
this.imageUrl,
this.userId,
});
FirebaseUserDocument.fromJson(
Map<String, Object?> json,
String userId,
) : this(
userId: userId,
firstName: json['first_name'] as String?,
lastName: json['last_name'] as String?,
imageUrl: json['image_url'] as String?,
);
final String? firstName;
final String? lastName;
final String? imageUrl;
final String? userId;
Map<String, Object?> toJson() => {
'first_name': firstName,
'last_name': lastName,
'image_url': imageUrl,
};
}

View file

@ -0,0 +1,138 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'dart:typed_data';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:flutter_timeline_firebase/config/firebase_timeline_options.dart';
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
class FirebaseTimelineService implements TimelineService {
FirebaseTimelineService({
required TimelineUserService userService,
FirebaseApp? app,
options = const FirebaseTimelineOptions(),
}) {
var appInstance = app ?? Firebase.app();
_db = FirebaseFirestore.instanceFor(app: appInstance);
_storage = FirebaseStorage.instanceFor(app: appInstance);
_userService = userService;
_options = options;
}
late FirebaseFirestore _db;
late FirebaseStorage _storage;
late TimelineUserService _userService;
late FirebaseTimelineOptions _options;
List<TimelinePost> _posts = [];
@override
Future<void> createPost(TimelinePost post) async {
var imageRef = _storage.ref().child('timeline/${post.id}');
var result = await imageRef.putData(post.image!);
var imageUrl = await result.ref.getDownloadURL();
var updatedPost = post.copyWith(imageUrl: imageUrl);
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
_posts.add(updatedPost);
return postRef.set(updatedPost.toJson());
}
@override
Future<void> deletePost(TimelinePost post) async {
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
return postRef.delete();
}
@override
Future<TimelinePost> fetchPostDetails(TimelinePost post) async {
debugPrint('fetchPostDetails');
return post;
}
@override
Future<List<TimelinePost>> fetchPosts(String? category) async {
var snapshot = await _db
.collection(_options.timelineCollectionName)
.where('category', isEqualTo: category)
.get();
var posts = <TimelinePost>[];
for (var doc in snapshot.docs) {
var data = doc.data();
var user = await _userService.getUser(data['user_id']);
var post = TimelinePost.fromJson(doc.id, data).copyWith(creator: user);
posts.add(post);
}
_posts = posts;
return posts;
}
@override
Future<void> likePost(String userId, TimelinePost post) {
// update the post with the new like
_posts = _posts
.map(
(p) => (p.id == post.id)
? p.copyWith(
likes: p.likes + 1,
likedBy: p.likedBy?..add(userId),
)
: p,
)
.toList();
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
return postRef.update({
'likes': FieldValue.increment(1),
'liked_by': FieldValue.arrayUnion([userId]),
});
}
@override
Future<void> unlikePost(String userId, TimelinePost post) {
// update the post with the new like
_posts = _posts
.map(
(p) => (p.id == post.id)
? p.copyWith(
likes: p.likes - 1,
likedBy: p.likedBy?..remove(userId),
)
: p,
)
.toList();
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
return postRef.update({
'likes': FieldValue.increment(-1),
'liked_by': FieldValue.arrayRemove([userId]),
});
}
@override
Future<void> reactToPost(
TimelinePost post,
TimelinePostReaction reaction, {
Uint8List? image,
}) {
// update the post with the new reaction
_posts = _posts
.map(
(p) => (p.id == post.id)
? p.copyWith(
reaction: p.reaction + 1,
reactions: p.reactions?..add(reaction),
)
: p,
)
.toList();
var postRef = _db.collection(_options.timelineCollectionName).doc(post.id);
return postRef.update({
'reaction': FieldValue.increment(1),
'reactions': FieldValue.arrayUnion([reaction.toJson()]),
});
}
}

View file

@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_timeline_firebase/config/firebase_timeline_options.dart';
import 'package:flutter_timeline_firebase/models/firebase_user_document.dart';
import 'package:flutter_timeline_interface/flutter_timeline_interface.dart';
class FirebaseUserService implements TimelineUserService {
FirebaseUserService({
FirebaseApp? app,
options = const FirebaseTimelineOptions(),
}) {
var appInstance = app ?? Firebase.app();
_db = FirebaseFirestore.instanceFor(app: appInstance);
_options = options;
}
late FirebaseFirestore _db;
late FirebaseTimelineOptions _options;
final Map<String, TimelinePosterUserModel> _users = {};
CollectionReference<FirebaseUserDocument> get _userCollection => _db
.collection(_options.usersCollectionName)
.withConverter<FirebaseUserDocument>(
fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson(
snapshot.data()!,
snapshot.id,
),
toFirestore: (user, _) => user.toJson(),
);
@override
Future<TimelinePosterUserModel?> getUser(String userId) async {
if (_users.containsKey(userId)) {
return _users[userId]!;
}
var data = (await _userCollection.doc(userId).get()).data();
var user = data == null
? TimelinePosterUserModel(userId: userId)
: TimelinePosterUserModel(
userId: userId,
firstName: data.firstName,
lastName: data.lastName,
imageUrl: data.imageUrl,
);
_users[userId] = user;
return user;
}
}

View file

@ -7,3 +7,6 @@ library flutter_timeline_interface;
export 'src/model/timeline_post.dart'; export 'src/model/timeline_post.dart';
export 'src/model/timeline_poster.dart'; export 'src/model/timeline_poster.dart';
export 'src/model/timeline_reaction.dart'; export 'src/model/timeline_reaction.dart';
export 'src/services/timeline_service.dart';
export 'src/services/user_service.dart';

View file

@ -2,6 +2,8 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_timeline_interface/src/model/timeline_poster.dart'; import 'package:flutter_timeline_interface/src/model/timeline_poster.dart';
import 'package:flutter_timeline_interface/src/model/timeline_reaction.dart'; import 'package:flutter_timeline_interface/src/model/timeline_reaction.dart';
@ -23,8 +25,37 @@ class TimelinePost {
this.likedBy, this.likedBy,
this.reactions, this.reactions,
this.imageUrl, this.imageUrl,
this.image,
}); });
factory TimelinePost.fromJson(String id, Map<String, dynamic> json) =>
TimelinePost(
id: id,
creatorId: json['creator_id'] as String,
title: json['title'] as String,
category: json['category'] as String,
imageUrl: json['image_url'] as String?,
content: json['content'] as String,
likes: json['likes'] as int,
likedBy: (json['liked_by'] as List<dynamic>?)?.cast<String>(),
reaction: json['reaction'] as int,
reactions: (json['reactions'] as Map<String, dynamic>?)
?.map(
(key, value) => MapEntry(
key,
TimelinePostReaction.fromJson(
key,
id,
value as Map<String, dynamic>,
),
),
)
.values
.toList(),
createdAt: DateTime.parse(json['created_at'] as String),
reactionEnabled: json['reaction_enabled'] as bool,
);
/// The unique identifier of the post. /// The unique identifier of the post.
final String id; final String id;
@ -43,6 +74,9 @@ class TimelinePost {
/// The url of the image of the post. /// The url of the image of the post.
final String? imageUrl; final String? imageUrl;
/// The image of the post used for uploading.
final Uint8List? image;
/// The content of the post. /// The content of the post.
final String content; final String content;
@ -63,4 +97,51 @@ class TimelinePost {
/// If reacting is enabled on the post. /// If reacting is enabled on the post.
final bool reactionEnabled; final bool reactionEnabled;
TimelinePost copyWith({
String? id,
String? creatorId,
TimelinePosterUserModel? creator,
String? title,
String? category,
String? imageUrl,
Uint8List? image,
String? content,
int? likes,
List<String>? likedBy,
int? reaction,
List<TimelinePostReaction>? reactions,
DateTime? createdAt,
bool? reactionEnabled,
}) =>
TimelinePost(
id: id ?? this.id,
creatorId: creatorId ?? this.creatorId,
creator: creator ?? this.creator,
title: title ?? this.title,
category: category ?? this.category,
imageUrl: imageUrl ?? this.imageUrl,
image: image ?? this.image,
content: content ?? this.content,
likes: likes ?? this.likes,
likedBy: likedBy ?? this.likedBy,
reaction: reaction ?? this.reaction,
reactions: reactions ?? this.reactions,
createdAt: createdAt ?? this.createdAt,
reactionEnabled: reactionEnabled ?? this.reactionEnabled,
);
Map<String, dynamic> toJson() => {
'creator_id': creatorId,
'title': title,
'category': category,
'image_url': imageUrl,
'content': content,
'likes': likes,
'liked_by': likedBy,
'reaction': reaction,
'reactions': reactions?.map((e) => e.toJson()).toList(),
'created_at': createdAt.toIso8601String(),
'reaction_enabled': reactionEnabled,
};
} }

View file

@ -7,13 +7,13 @@ import 'package:flutter/material.dart';
@immutable @immutable
class TimelinePosterUserModel { class TimelinePosterUserModel {
const TimelinePosterUserModel({ const TimelinePosterUserModel({
required this.id, required this.userId,
this.firstName, this.firstName,
this.lastName, this.lastName,
this.imageUrl, this.imageUrl,
}); });
final String id; final String userId;
final String? firstName; final String? firstName;
final String? lastName; final String? lastName;
final String? imageUrl; final String? imageUrl;

View file

@ -17,6 +17,20 @@ class TimelinePostReaction {
this.creator, this.creator,
}); });
factory TimelinePostReaction.fromJson(
String id,
String postId,
Map<String, dynamic> json,
) =>
TimelinePostReaction(
id: id,
postId: postId,
creatorId: json['creator_id'] as String,
reaction: json['reaction'] as String?,
imageUrl: json['image_url'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
);
/// The unique identifier of the reaction. /// The unique identifier of the reaction.
final String id; final String id;
@ -37,4 +51,13 @@ class TimelinePostReaction {
/// Reaction creation date. /// Reaction creation date.
final DateTime createdAt; final DateTime createdAt;
Map<String, dynamic> toJson() => <String, dynamic>{
id: {
'creator_id': creatorId,
'reaction': reaction,
'image_url': imageUrl,
'created_at': createdAt.toIso8601String(),
},
};
} }

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_timeline_interface/src/model/timeline_post.dart'; import 'package:flutter_timeline_interface/src/model/timeline_post.dart';
@ -13,5 +17,6 @@ abstract class TimelineService {
TimelinePostReaction reaction, { TimelinePostReaction reaction, {
Uint8List image, Uint8List image,
}); });
Future<void> likePost(TimelinePost post); Future<void> likePost(String userId, TimelinePost post);
Future<void> unlikePost(String userId, TimelinePost post);
} }

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter_timeline_interface/src/model/timeline_poster.dart'; import 'package:flutter_timeline_interface/src/model/timeline_poster.dart';
mixin TimelineUserService { mixin TimelineUserService {