275 lines
12 KiB
Dart
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")),
|
|
]),
|
|
])));
|
|
}
|
|
} |