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_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';
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(),
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue