omni_chat 1.0.3
omni_chat: ^1.0.3 copied to clipboard
A professional, feature-rich Flutter chat UI package. Supports 17+ message types including text, image, video, audio waveform, sticker, GIF, document, location, contact, link preview, poll, profile ca [...]
example/lib/main.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:omni_chat/omni_chat.dart';
void main() => runApp(const OmniChatExampleApp());
class OmniChatExampleApp extends StatelessWidget {
const OmniChatExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'OmniChat Demo',
theme: ThemeData.dark(useMaterial3: true),
home: const ChatScreen(),
debugShowCheckedModeBanner: false,
);
}
}
// ── Mock data ─────────────────────────────────────────────────────────────────
const _me = 'user_me';
const _other = 'user_them';
int _msgId = 0;
String _nextId() => '${++_msgId}';
final List<ChatMessage> _initialMessages = [
SystemMessage(
id: _nextId(),
senderId: 'system',
timestamp: DateTime.now().subtract(const Duration(hours: 2)),
text: 'Sohbet başladı • Uçtan uca şifreli',
),
TextMessage(
id: _nextId(),
senderId: _other,
timestamp: DateTime.now().subtract(const Duration(hours: 1, minutes: 50)),
text: 'Merhaba! Bugün nasılsın?',
status: MessageStatus.read,
),
TextMessage(
id: _nextId(),
senderId: _me,
timestamp: DateTime.now().subtract(const Duration(hours: 1, minutes: 48)),
text: 'İyiyim, teşekkürler! Sen nasılsın? 😊',
status: MessageStatus.read,
reactions: [
MessageReaction(emoji: '❤️', userIds: [_other]),
],
),
AudioMessage(
id: _nextId(),
senderId: _other,
timestamp: DateTime.now().subtract(const Duration(hours: 1, minutes: 40)),
audioUrl: 'https://example.com/audio/voice.m4a',
duration: 305,
waveformData: List.generate(
40,
(i) => 0.2 + 0.7 * (sin(i * 0.4).abs()),
),
source: MediaSource.microphone,
status: MessageStatus.read,
),
ImageMessage(
id: _nextId(),
senderId: _other,
timestamp: DateTime.now().subtract(const Duration(hours: 1, minutes: 30)),
imageUrl:
'https://images.unsplash.com/photo-1541961017774-22349e4a1262?w=600',
caption: 'Bugün çektiğim fotoğraf 📷',
source: MediaSource.camera,
status: MessageStatus.read,
),
ImageMessage(
id: _nextId(),
senderId: _me,
timestamp: DateTime.now().subtract(const Duration(hours: 1, minutes: 20)),
imageUrl:
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=600',
caption: 'Benim de dünden bir fotoğrafım',
source: MediaSource.gallery,
status: MessageStatus.read,
),
LinkPreviewMessage(
id: _nextId(),
senderId: _other,
timestamp: DateTime.now().subtract(const Duration(hours: 1, minutes: 10)),
url: 'https://dribbble.com/shots/21436621',
title: 'Amazing UI Design Shots',
description: 'Explore the best UI designs on Dribbble.',
siteName: 'dribbble.com',
imageUrl:
'https://images.unsplash.com/photo-1558655146-9f40138edfeb?w=600',
text: 'Bunu gördün mü? Harika tasarımlar!',
status: MessageStatus.read,
),
ProfileCardMessage(
id: _nextId(),
senderId: _other,
timestamp: DateTime.now().subtract(const Duration(minutes: 50)),
userId: _other,
username: 'odama_studio',
displayName: 'Odama Studio',
avatarUrl:
'https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=200',
isVerified: true,
bio: 'Professional UI/UX design studio',
status: MessageStatus.read,
),
PollMessage(
id: _nextId(),
senderId: _me,
timestamp: DateTime.now().subtract(const Duration(minutes: 40)),
question: 'Bu hafta sonu buluşalım mı?',
options: [
PollOption(id: 'a', text: 'Cumartesi 🎉', voterIds: [_other]),
PollOption(id: 'b', text: 'Pazar ☀️', voterIds: []),
PollOption(id: 'c', text: 'Uygun değilim ❌', voterIds: []),
],
status: MessageStatus.read,
),
EphemeralImageMessage(
id: _nextId(),
senderId: _other,
timestamp: DateTime.now().subtract(const Duration(minutes: 30)),
imageUrl:
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=600',
hasBeenViewed: false,
status: MessageStatus.delivered,
),
DocumentMessage(
id: _nextId(),
senderId: _me,
timestamp: DateTime.now().subtract(const Duration(minutes: 20)),
fileUrl: 'https://example.com/docs/report.pdf',
fileName: 'Q4_Report_2024.pdf',
fileSize: 2_450_000,
mimeType: 'application/pdf',
status: MessageStatus.sent,
),
TextMessage(
id: _nextId(),
senderId: _other,
timestamp: DateTime.now().subtract(const Duration(minutes: 5)),
text: 'Harika! Dokümanı aldım, teşekkürler 👍',
status: MessageStatus.read,
),
];
// ── Chat screen ───────────────────────────────────────────────────────────────
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final List<ChatMessage> _messages = List.from(_initialMessages);
ChatMessage? _replyTo;
ChatMessage? _editMessage;
bool _isDark = true;
void _addMessage(ChatMessage msg) {
setState(() => _messages.add(msg));
}
void _sendText(String text) {
_addMessage(TextMessage(
id: _nextId(),
senderId: _me,
timestamp: DateTime.now(),
text: text,
replyTo: _replyTo,
status: MessageStatus.sending,
));
setState(() => _replyTo = null);
}
void _sendAudio(String path, int duration, List<double> wave) {
_addMessage(AudioMessage(
id: _nextId(),
senderId: _me,
timestamp: DateTime.now(),
audioUrl: path,
duration: duration,
waveformData: wave,
source: MediaSource.microphone,
status: MessageStatus.sending,
));
}
@override
Widget build(BuildContext context) {
final theme = _isDark ? OmniChatTheme.dark() : OmniChatTheme.light();
return OmniChatThemeScope(
theme: theme,
child: Scaffold(
appBar: AppBar(
backgroundColor: _isDark ? const Color(0xFF1F2C34) : Colors.white,
leading: IconButton(
icon: Icon(Icons.arrow_back,
color: _isDark ? Colors.white : Colors.black87),
onPressed: () {},
),
title: Row(
children: [
const CircleAvatar(
radius: 18,
backgroundColor: Color(0xFF00A884),
child: Text('OS',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold)),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Odama Studio',
style: TextStyle(
color: _isDark ? Colors.white : Colors.black87,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Text(
'Çevrimiçi',
style: TextStyle(
color: _isDark ? Colors.white60 : Colors.grey,
fontSize: 12,
),
),
],
),
],
),
actions: [
IconButton(
icon: Icon(
_isDark ? Icons.light_mode : Icons.dark_mode,
color: _isDark ? Colors.white : Colors.black87,
),
onPressed: () => setState(() => _isDark = !_isDark),
tooltip: 'Temayı değiştir',
),
IconButton(
icon: Icon(Icons.more_vert,
color: _isDark ? Colors.white : Colors.black87),
onPressed: () {},
),
],
),
body: Column(
children: [
Expanded(
child: OmniChatList(
messages: _messages,
currentUserId: _me,
isEncrypted: true,
screenshotProtection: false, // set true in production
getSenderName: (uid) =>
uid == _me ? 'Sen' : 'Odama Studio',
onReply: (msg) => setState(() => _replyTo = msg),
onEdit: (msg) => setState(() => _editMessage = msg),
onDelete: (msg, forAll) {
setState(() {
final i = _messages.indexWhere((m) => m.id == msg.id);
if (i != -1) {
_messages[i] = msg.copyWith(isDeleted: true);
}
});
},
onCopy: (_) {},
onForward: (_) {},
onPin: (_) {},
onReact: (msg, emoji) {
setState(() {
final i = _messages.indexWhere((m) => m.id == msg.id);
if (i == -1) return;
final existing = _messages[i].reactions;
final reactionIdx =
existing.indexWhere((r) => r.emoji == emoji);
List<MessageReaction> updated;
if (reactionIdx != -1) {
final r = existing[reactionIdx];
if (r.userIds.contains(_me)) {
updated = [
...existing.sublist(0, reactionIdx),
r.copyWith(
userIds: r.userIds
.where((id) => id != _me)
.toList()),
...existing.sublist(reactionIdx + 1),
];
} else {
updated = [
...existing.sublist(0, reactionIdx),
r.copyWith(userIds: [...r.userIds, _me]),
...existing.sublist(reactionIdx + 1),
];
}
} else {
updated = [
...existing,
MessageReaction(emoji: emoji, userIds: [_me]),
];
}
updated.removeWhere((r) => r.userIds.isEmpty);
_messages[i] = _messages[i].copyWith(reactions: updated);
});
},
onProfileTap: (userId) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Profil: $userId')),
);
},
onPollVote: (msg, optId) {
setState(() {
final i = _messages.indexWhere((m) => m.id == msg.id);
if (i == -1 || _messages[i] is! PollMessage) return;
final poll = _messages[i] as PollMessage;
final opts = poll.options.map((o) {
if (o.id == optId && !o.voterIds.contains(_me)) {
return o.copyWith(voterIds: [...o.voterIds, _me]);
}
return o;
}).toList();
_messages[i] = poll.copyWith(options: opts);
});
},
),
),
OmniChatInput(
replyMessage: _replyTo,
replyMessageSenderName: _replyTo != null
? (_replyTo!.senderId == _me ? 'Sen' : 'Odama Studio')
: null,
onCancelReply: () => setState(() => _replyTo = null),
editMessage: _editMessage,
onCancelEdit: () => setState(() => _editMessage = null),
onSendEdit: (text) {
setState(() {
if (_editMessage == null) return;
final i = _messages.indexWhere((m) => m.id == _editMessage!.id);
if (i != -1) {
_messages[i] = _editMessage!.copyWith(isEdited: true);
}
_editMessage = null;
});
},
onSendText: _sendText,
onSendAudio: _sendAudio,
onAttachment: (type) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Ek seçildi: ${type.name}')),
);
},
),
],
),
),
);
}
}