Main Dart
Main Dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:record/record.dart';
void main() {
runApp(const MyApp());
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Kafgram',
theme: ThemeData(
primaryColor: const Color(0xFF527DA3), // Telegram blue
scaffoldBackgroundColor:
const Color(0xFFEFEFEF), // Light grey background
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF527DA3),
foregroundColor: Colors.white,
elevation: 0,
),
textTheme: const TextTheme(
bodyMedium: TextStyle(fontFamily: 'Roboto', color: Colors.black87),
titleLarge:
TextStyle(fontFamily: 'Roboto', fontWeight: FontWeight.bold),
),
fontFamily: 'Roboto',
colorScheme: ColorScheme.fromSwatch().copyWith(
secondary: const Color(0xFF65AADD), // Accent color
),
),
home: const AuthCheckPage(),
);
}
}
@override
_AuthCheckPageState createState() => _AuthCheckPageState();
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
}
@override
_LoginPageState createState() => _LoginPageState();
}
try {
final response = await http.post(
Uri.parse('http://192.168.1.2:8000/request_code'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'phone': phone}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['status'] == 'code_sent') {
setState(() {
_codeSent = true;
_phoneCodeHash = data['phone_code_hash'];
_isLoading = false;
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Failed to send code',
style: TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
setState(() {
_isLoading = false;
});
}
} else {
final data = jsonDecode(response.body);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Error: ${data['detail'] ?? 'Unknown error'}',
style: const TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
setState(() {
_isLoading = false;
});
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Network error. Please try again.',
style: TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
setState(() {
_isLoading = false;
});
}
}
try {
final response = await http.post(
Uri.parse('http://192.168.1.2:8000/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'phone': phone,
'code': code,
'phone_code_hash': _phoneCodeHash,
'password': password.isEmpty ? null : password,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['status'] == 'logged_in') {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('phone', phone);
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => ChatsPage(phone: phone)),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Login failed: ${data['detail'] ?? 'Unknown error'}',
style: const TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
}
} else {
final data = jsonDecode(response.body);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Error: ${data['detail'] ?? 'Login failed'}',
style: const TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Network error. Please try again.',
style: TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
}
setState(() {
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Kafgram',
style: TextStyle(fontFamily: 'Roboto', fontWeight: FontWeight.w500),
),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!_codeSent) ...[
TextField(
controller: _phoneController,
decoration: InputDecoration(
labelText: 'Phone Number',
hintText: 'e.g., +1234567890',
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 20),
_isLoading
? const CircularProgressIndicator()
: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF65AADD),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(
horizontal: 40, vertical: 15),
),
onPressed: _sendPhoneNumber,
child: const Text(
'Send Code',
style: TextStyle(fontFamily: 'Roboto', fontSize: 16),
),
),
] else ...[
TextField(
controller: _codeController,
decoration: InputDecoration(
labelText: 'Verification Code',
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 20),
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText: '2FA Password (optional)',
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
),
obscureText: true,
),
const SizedBox(height: 20),
_isLoading
? const CircularProgressIndicator()
: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF65AADD),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(
horizontal: 40, vertical: 15),
),
onPressed: _verifyCode,
child: const Text(
'Verify Code',
style: TextStyle(fontFamily: 'Roboto', fontSize: 16),
),
),
],
],
),
),
);
}
@override
void dispose() {
_phoneController.dispose();
_codeController.dispose();
_passwordController.dispose();
super.dispose();
}
}
// Chats Page
class ChatsPage extends StatefulWidget {
final String phone;
@override
_ChatsPageState createState() => _ChatsPageState();
}
@override
void initState() {
super.initState();
_fetchChats();
}
try {
final response = await http.get(
Uri.parse(
'http://192.168.1.2:8000/chats?phone=$
{Uri.encodeComponent(widget.phone)}'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
// Sort chats: pinned first, then by timestamp (oldest first)
List<dynamic> sortedChats = data['chats'];
sortedChats.sort((a, b) {
// Prioritize pinned chats
if (a['is_pinned'] && !b['is_pinned']) return -1;
if (!a['is_pinned'] && b['is_pinned']) return 1;
// If both are pinned or both are not pinned, sort by timestamp (oldest
first)
int aTimestamp = a['timestamp'] ?? 0;
int bTimestamp = b['timestamp'] ?? 0;
if (aTimestamp == 0 && bTimestamp == 0) {
// Fallback to last_message presence (chats with messages come first)
if (a['last_message'] != '' && b['last_message'] == '') return -1;
if (a['last_message'] == '' && b['last_message'] != '') return 1;
return 0;
}
// Ensure chats with no timestamp go to the end
if (aTimestamp == 0) return 1;
if (bTimestamp == 0) return -1;
return aTimestamp.compareTo(bTimestamp); // Oldest first
});
setState(() {
_chats = sortedChats;
_isLoading = false;
});
} else {
final data = jsonDecode(response.body);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Error: ${data['detail'] ?? 'Failed to fetch chats'}',
style: const TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
if (response.statusCode == 401) {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove('phone');
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
);
}
}
setState(() {
_isLoading = false;
});
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Failed to fetch chats. Please check your connection.',
style: TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Chats',
style: TextStyle(fontFamily: 'Roboto', fontWeight: FontWeight.w500),
),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: _logout,
tooltip: 'Logout',
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _chats.isEmpty
? const Center(
child: Text(
'No chats available',
style: TextStyle(fontFamily: 'Roboto', fontSize: 16),
),
)
: ListView.builder(
itemCount: _chats.length,
itemBuilder: (context, index) {
final chat = _chats[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
leading: CircleAvatar(
radius: 28,
backgroundColor: Colors.grey[300],
child: chat['photo'] != ''
? ClipOval(
child: Image.network(
'http://192.168.1.2:8000/${chat['photo']}',
width: 56,
height: 56,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
Text(
chat['name'][0],
style: const TextStyle(
fontFamily: 'Roboto',
fontSize: 20,
color: Colors.white),
),
),
)
: Text(
chat['name'][0],
style: const TextStyle(
fontFamily: 'Roboto',
fontSize: 20,
color: Colors.white),
),
),
title: Text(
chat['name'],
style: const TextStyle(
fontFamily: 'Roboto',
fontWeight: FontWeight.w500,
fontSize: 16,
),
),
subtitle: Text(
chat['last_message'] ?? 'No messages',
style: TextStyle(
fontFamily: 'Roboto',
color: Colors.grey[600],
fontSize: 14,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: chat['unread_count'] > 0
? CircleAvatar(
radius: 12,
backgroundColor: const Color(0xFF65AADD),
child: Text(
'${chat['unread_count']}',
style: const TextStyle(
fontFamily: 'Roboto',
color: Colors.white,
fontSize: 12,
),
),
)
: null,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatPage(
phone: widget.phone,
chatId: chat['id'],
chatName: chat['name'],
onMessageSent: _fetchChats,
),
),
);
},
);
},
),
);
}
}
const ChatPage({
super.key,
required this.phone,
required this.chatId,
required this.chatName,
required this.onMessageSent,
});
@override
_ChatPageState createState() => _ChatPageState();
}
@override
void initState() {
super.initState();
_fetchMessages();
_initRecorder();
}
try {
final response = await http.get(
Uri.parse(
'http://192.168.1.2:8000/messages?phone=$
{Uri.encodeComponent(widget.phone)}&chat_id=${widget.chatId}'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
_messages = data['messages'];
_isLoading = false;
});
} else {
final data = jsonDecode(response.body);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Error: ${data['detail'] ?? 'Failed to fetch messages'}',
style: const TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
if (response.statusCode == 401) {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove('phone');
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
);
}
}
setState(() {
_isLoading = false;
});
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Failed to fetch messages. Please check your connection.',
style: TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
setState(() {
_isLoading = false;
});
}
}
setState(() {
_isLoading = true;
});
try {
final response = await http.post(
Uri.parse('http://192.168.1.2:8000/send_message'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'phone': widget.phone,
'recipient': widget.chatId,
'message': message,
}),
);
if (response.statusCode == 200) {
_messageController.clear();
await _fetchMessages();
widget.onMessageSent();
} else {
final data = jsonDecode(response.body);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Error: ${data['detail'] ?? 'Failed to send message'}',
style: const TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Failed to send message. Please check your connection.',
style: TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
}
setState(() {
_isLoading = false;
});
}
setState(() {
_isLoading = true;
});
try {
var request = http.MultipartRequest(
'POST',
Uri.parse('http://192.168.1.2:8000/send_voice'),
);
request.fields['phone'] = widget.phone;
request.fields['recipient'] = widget.chatId;
request.files.add(
await http.MultipartFile.fromPath('file_path', _recordedFilePath!),
);
final response = await request.send();
final responseData = await response.stream.bytesToString();
if (response.statusCode == 200) {
await _fetchMessages();
widget.onMessageSent();
} else {
final data = jsonDecode(responseData);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Error: ${data['detail'] ?? 'Failed to send voice message'}',
style: const TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Failed to send voice message: $e',
style: const TextStyle(fontFamily: 'Roboto'),
),
backgroundColor: Colors.redAccent,
),
);
} finally {
if (_recordedFilePath != null &&
await File(_recordedFilePath!).exists()) {
await File(_recordedFilePath!).delete();
}
setState(() {
_recordedFilePath = null;
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
widget.chatName,
style: const TextStyle(
fontFamily: 'Roboto', fontWeight: FontWeight.w500),
),
),
body: Column(
children: [
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _messages.isEmpty
? const Center(
child: Text(
'No messages available',
style: TextStyle(fontFamily: 'Roboto', fontSize: 16),
),
)
: ListView.builder(
reverse: true,
itemCount: _messages.length,
itemBuilder: (context, index) {
final message =
_messages[_messages.length - 1 - index];
final isSentByUser =
message['is_sent_by_user'] ?? false;
return Align(
alignment: isSentByUser
? Alignment.centerRight
: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 4, horizontal: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSentByUser
? const Color(0xFFD2E8FF)
: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: isSentByUser
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
if (message['is_voice'] &&
message['file_path'] != null)
Text(
'[Voice Message]',
style: const TextStyle(
fontFamily: 'Roboto',
fontSize: 15,
color: Colors.blueAccent,
),
)
else
Text(
message['text'],
style: const TextStyle(
fontFamily: 'Roboto',
fontSize: 15,
),
),
const SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
DateFormat('HH:mm').format(
DateTime.fromMillisecondsSinceEpoch(
message['timestamp']),
),
style: TextStyle(
fontFamily: 'Roboto',
fontSize: 12,
color: Colors.grey[600],
),
),
if (isSentByUser) ...[
const SizedBox(width: 4),
Icon(
message['is_read']
? Icons.done_all
: Icons.done,
size: 16,
color: Colors.grey[600],
),
],
],
),
],
),
),
);
},
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
color: Colors.white,
child: Row(
children: [
Expanded(
child: TextField(
controller: _messageController,
decoration: InputDecoration(
hintText: 'Message',
filled: true,
fillColor: const Color(0xFFF5F5F5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 10),
),
style: const TextStyle(fontFamily: 'Roboto'),
onSubmitted: (_) => _sendMessage(),
),
),
GestureDetector(
onLongPress: _startRecording,
onLongPressUp: _stopRecording,
child: IconButton(
icon: Icon(
_isRecording ? Icons.mic : Icons.mic_none,
color:
_isRecording ? Colors.red : const Color(0xFF65AADD),
),
onPressed:
() {}, // Empty callback to satisfy IconButton requirement
tooltip: 'Hold to Record Voice',
),
),
IconButton(
icon: const Icon(
Icons.send,
color: Color(0xFF65AADD),
),
onPressed: _sendMessage,
tooltip: 'Send Message',
),
],
),
),
],
),
);
}
@override
void dispose() {
_messageController.dispose();
_audioRecorder.dispose(); // Dispose AudioRecorder
super.dispose();
}
}