Initial commit
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:firebase_storage/firebase_storage.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:onsolgo/core/constants.dart';
|
||||
import 'package:onsolgo/core/share_helper.dart';
|
||||
import 'package:onsolgo/widgets/comments_sheet.dart';
|
||||
|
||||
class SocialFeed extends StatelessWidget {
|
||||
const SocialFeed({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(title: const Text("MANAA SOCIAL", style: TextStyle(letterSpacing: 2, fontWeight: FontWeight.bold)), centerTitle: true),
|
||||
body: StreamBuilder<QuerySnapshot>(
|
||||
stream: FirebaseFirestore.instance.collection('social_posts').orderBy('timestamp', descending: true).snapshots(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator(color: kOnsolGold));
|
||||
return ListView.builder(
|
||||
itemCount: snapshot.data!.docs.length,
|
||||
itemBuilder: (context, index) => _SocialPostCard(post: snapshot.data!.docs[index]),
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
backgroundColor: kOnsolGold, child: const Icon(Icons.add_a_photo, color: Colors.black),
|
||||
onPressed: () => showModalBottomSheet(context: context, isScrollControlled: true, backgroundColor: Colors.grey[900], builder: (c) => const _CreatePostSheet()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SocialPostCard extends StatefulWidget {
|
||||
final QueryDocumentSnapshot post;
|
||||
const _SocialPostCard({required this.post});
|
||||
|
||||
@override
|
||||
State<_SocialPostCard> createState() => _SocialPostCardState();
|
||||
}
|
||||
|
||||
class _SocialPostCardState extends State<_SocialPostCard> {
|
||||
final GlobalKey _postKey = GlobalKey();
|
||||
|
||||
Future<void> _confirmDeletePost() async {
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (c) => AlertDialog(
|
||||
backgroundColor: Colors.grey[900],
|
||||
title: const Text('Delete post?', style: TextStyle(color: Colors.white)),
|
||||
content: const Text('This cannot be undone.', style: TextStyle(color: Colors.white70)),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(c, false), child: const Text('Cancel')),
|
||||
TextButton(onPressed: () => Navigator.pop(c, true), child: const Text('Delete', style: TextStyle(color: Colors.redAccent))),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok != true || !mounted) return;
|
||||
try {
|
||||
await widget.post.reference.delete();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Post removed')));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Could not delete: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleLike() async {
|
||||
final uid = FirebaseAuth.instance.currentUser?.uid;
|
||||
if (uid == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Sign in to like posts')));
|
||||
return;
|
||||
}
|
||||
final ref = widget.post.reference;
|
||||
try {
|
||||
await FirebaseFirestore.instance.runTransaction((txn) async {
|
||||
final snap = await txn.get(ref);
|
||||
final data = snap.data() as Map<String, dynamic>?;
|
||||
final liked = List<String>.from((data?['likedBy'] as List?)?.map((e) => e.toString()) ?? []);
|
||||
final set = liked.toSet();
|
||||
if (set.contains(uid)) {
|
||||
set.remove(uid);
|
||||
} else {
|
||||
set.add(uid);
|
||||
}
|
||||
final next = set.toList();
|
||||
txn.update(ref, {'likedBy': next, 'likes': next.length});
|
||||
});
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Like failed: $e')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final post = widget.post;
|
||||
final d = post.data() as Map<String, dynamic>;
|
||||
final bool isVer = (d['isVerified'] ?? false) || (d['authorRank'] == "VERIFIED ManaA ARTIST");
|
||||
final uid = FirebaseAuth.instance.currentUser?.uid ?? '';
|
||||
final likedBy = List<String>.from((d['likedBy'] as List?)?.map((e) => e.toString()) ?? []);
|
||||
final liked = uid.isNotEmpty && likedBy.contains(uid);
|
||||
final likeCount = likedBy.length;
|
||||
final authorId = (d['authorId'] as String?) ?? '';
|
||||
|
||||
Widget card({required bool canDelete}) {
|
||||
return RepaintBoundary(
|
||||
key: _postKey,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(color: Colors.grey[900], borderRadius: BorderRadius.circular(15)),
|
||||
child: Column(children: [
|
||||
StreamBuilder<DocumentSnapshot>(
|
||||
stream: FirebaseFirestore.instance.collection('users').doc(d['authorId']).snapshots(),
|
||||
builder: (context, userSnap) {
|
||||
String? livePfp;
|
||||
if (userSnap.hasData && userSnap.data!.exists) {
|
||||
var uD = userSnap.data!.data() as Map;
|
||||
livePfp = uD.containsKey('pfpUrl') ? uD['pfpUrl'] : null;
|
||||
}
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.black,
|
||||
backgroundImage: livePfp != null ? NetworkImage(livePfp) : null,
|
||||
child: livePfp == null ? const Icon(Icons.person, color: Colors.white24) : null),
|
||||
title: Row(children: [
|
||||
Text(d['authorName'] ?? 'Citizen', style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white)),
|
||||
verifiedBadge(isVer, size: 18),
|
||||
]),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (canDelete)
|
||||
IconButton(
|
||||
tooltip: 'Delete post',
|
||||
icon: const Icon(Icons.delete_outline, size: 20, color: Colors.redAccent),
|
||||
onPressed: _confirmDeletePost,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share_outlined, size: 18, color: Colors.grey),
|
||||
onPressed: () {
|
||||
final raw = (d['content'] as String?)?.trim() ?? '';
|
||||
final caption = raw.isNotEmpty
|
||||
? raw
|
||||
: 'Transmission from ${d['authorName'] ?? 'ONSOL-GO'}';
|
||||
OnsolShare.share(
|
||||
_postKey,
|
||||
caption,
|
||||
link: kOnsolSharePostUrl(post.id),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (d['imageUrl'] != null) CachedNetworkImage(imageUrl: d['imageUrl'], width: double.infinity, fit: BoxFit.cover),
|
||||
if (d['content'] != "") Padding(padding: const EdgeInsets.all(16), child: Text(d['content'] ?? '')),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 0, 8, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(liked ? Icons.favorite : Icons.favorite_border, color: liked ? Colors.redAccent : Colors.white70, size: 22),
|
||||
onPressed: _toggleLike,
|
||||
tooltip: 'Like',
|
||||
),
|
||||
Text('$likeCount', style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chat_bubble_outline, color: Colors.white70, size: 22),
|
||||
onPressed: () => CommentsSheet.show(context, parent: post.reference, title: 'Comments'),
|
||||
tooltip: 'Comments',
|
||||
),
|
||||
const Text('Comment', style: TextStyle(color: Colors.white54, fontSize: 13)),
|
||||
],
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (uid.isEmpty) {
|
||||
return card(canDelete: false);
|
||||
}
|
||||
|
||||
return StreamBuilder<DocumentSnapshot>(
|
||||
stream: FirebaseFirestore.instance.collection('users').doc(uid).snapshots(),
|
||||
builder: (context, meSnap) {
|
||||
String? role;
|
||||
if (meSnap.hasData && meSnap.data!.exists) {
|
||||
role = (meSnap.data!.data() as Map?)?['role'] as String?;
|
||||
}
|
||||
final canDelete = uid == authorId || role == 'admin';
|
||||
return card(canDelete: canDelete);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreatePostSheet extends StatefulWidget {
|
||||
const _CreatePostSheet();
|
||||
@override
|
||||
State<_CreatePostSheet> createState() => _CreatePostSheetState();
|
||||
}
|
||||
|
||||
class _CreatePostSheetState extends State<_CreatePostSheet> {
|
||||
final _textC = TextEditingController(); Uint8List? _webImage; bool _posting = false;
|
||||
Future<void> _pick() async {
|
||||
final picked = await ImagePicker().pickImage(source: ImageSource.gallery, imageQuality: 60);
|
||||
if (picked != null) { final bytes = await picked.readAsBytes(); setState(() => _webImage = bytes); }
|
||||
}
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(child: Padding(padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom + 20, left: 20, right: 20, top: 20), child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Text("NEW ANNOUNCEMENT", style: TextStyle(fontWeight: FontWeight.bold, color: kOnsolGold)),
|
||||
if (_webImage != null) Image.memory(_webImage!, height: 100),
|
||||
TextField(controller: _textC, maxLines: 3, style: const TextStyle(color: Colors.white), decoration: const InputDecoration(hintText: "Speak to the Order...")),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
IconButton(onPressed: _pick, icon: const Icon(Icons.image, color: Colors.amber)),
|
||||
_posting ? const CircularProgressIndicator() : ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: kOnsolGold, foregroundColor: Colors.black),
|
||||
onPressed: () async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
setState(() => _posting = true);
|
||||
try {
|
||||
final u = FirebaseAuth.instance.currentUser;
|
||||
final uSnap = await FirebaseFirestore.instance.collection('users').doc(u?.uid).get();
|
||||
final raw = uSnap.data();
|
||||
if (u == null || !uSnap.exists || raw == null) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(content: Text('Profile not found. Try signing in again.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final uMap = Map<String, dynamic>.from(raw as Map);
|
||||
String? url;
|
||||
if (_webImage != null) {
|
||||
final ref = FirebaseStorage.instance.ref().child('social').child('${DateTime.now().millisecondsSinceEpoch}.jpg');
|
||||
await ref.putData(_webImage!, SettableMetadata(contentType: 'image/jpeg'));
|
||||
url = await ref.getDownloadURL();
|
||||
}
|
||||
await FirebaseFirestore.instance.collection('social_posts').add({
|
||||
'authorId': u.uid,
|
||||
'authorName': uMap['username'],
|
||||
'authorPfp': uMap['pfpUrl'],
|
||||
'authorRank': getRankName(safeInt(uMap['rankLevel'])),
|
||||
'isVerified': safeInt(uMap['rankLevel']) == 5 || (uMap['isVerified'] ?? false),
|
||||
'content': _textC.text,
|
||||
'imageUrl': url,
|
||||
'likes': 0,
|
||||
'likedBy': [],
|
||||
'timestamp': FieldValue.serverTimestamp(),
|
||||
});
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(SnackBar(content: Text('Post failed: $e')));
|
||||
} finally {
|
||||
if (mounted) setState(() => _posting = false);
|
||||
}
|
||||
},
|
||||
child: const Text("POST")),
|
||||
]),
|
||||
])));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user