mirror of
https://github.com/Iconica-Development/flutter_timeline.git
synced 2025-05-19 10:33:44 +02:00
feat: add firebase implementation of timeline service
This commit is contained in:
parent
d24731412f
commit
4113e9fea2
10 changed files with 366 additions and 3 deletions
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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()]),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -7,3 +7,6 @@ library flutter_timeline_interface;
|
|||
export 'src/model/timeline_post.dart';
|
||||
export 'src/model/timeline_poster.dart';
|
||||
export 'src/model/timeline_reaction.dart';
|
||||
|
||||
export 'src/services/timeline_service.dart';
|
||||
export 'src/services/user_service.dart';
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_timeline_interface/src/model/timeline_poster.dart';
|
||||
import 'package:flutter_timeline_interface/src/model/timeline_reaction.dart';
|
||||
|
@ -23,8 +25,37 @@ class TimelinePost {
|
|||
this.likedBy,
|
||||
this.reactions,
|
||||
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.
|
||||
final String id;
|
||||
|
||||
|
@ -43,6 +74,9 @@ class TimelinePost {
|
|||
/// The url of the image of the post.
|
||||
final String? imageUrl;
|
||||
|
||||
/// The image of the post used for uploading.
|
||||
final Uint8List? image;
|
||||
|
||||
/// The content of the post.
|
||||
final String content;
|
||||
|
||||
|
@ -63,4 +97,51 @@ class TimelinePost {
|
|||
|
||||
/// If reacting is enabled on the post.
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,13 +7,13 @@ import 'package:flutter/material.dart';
|
|||
@immutable
|
||||
class TimelinePosterUserModel {
|
||||
const TimelinePosterUserModel({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.imageUrl,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String userId;
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final String? imageUrl;
|
||||
|
|
|
@ -17,6 +17,20 @@ class TimelinePostReaction {
|
|||
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.
|
||||
final String id;
|
||||
|
||||
|
@ -37,4 +51,13 @@ class TimelinePostReaction {
|
|||
|
||||
/// Reaction creation date.
|
||||
final DateTime createdAt;
|
||||
|
||||
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||
id: {
|
||||
'creator_id': creatorId,
|
||||
'reaction': reaction,
|
||||
'image_url': imageUrl,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: 2023 Iconica
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_timeline_interface/src/model/timeline_post.dart';
|
||||
|
@ -13,5 +17,6 @@ abstract class TimelineService {
|
|||
TimelinePostReaction reaction, {
|
||||
Uint8List image,
|
||||
});
|
||||
Future<void> likePost(TimelinePost post);
|
||||
Future<void> likePost(String userId, TimelinePost post);
|
||||
Future<void> unlikePost(String userId, TimelinePost post);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: 2023 Iconica
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import 'package:flutter_timeline_interface/src/model/timeline_poster.dart';
|
||||
|
||||
mixin TimelineUserService {
|
||||
|
|
Loading…
Reference in a new issue