Files
Onsol-GO/lib/screens/social/social_feed.dart
T
2026-04-23 23:58:59 -05:00

275 lines
12 KiB
Dart

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")),
]),
])));
}
}