Screens / messaging
Chat thread
Shows a conversation anchored at the bottom, grouped by author and day, with older messages loaded while scrolling up, optimistic sending with retry, typing, and copy, reply or delete.
Import
Chat thread is part of Fliin Pro. With a license key, sign in once with fliin login, then add it in a Flutter app where fliin init has run:
fliin login
fliin add chatAlso installs: alert, app-bar, composer, confirm-delete, core, empty-state, link-button, message-bubble, separator, skeleton, spinner, avatar, button, icons
Examples
Thread
A conversation and its messages, grouped by day and by author. standalone holds its own controller over the service you pass; give it yours to reach your backend.
import 'package:your_app/ui/fliin/screens/chat/chat_controller.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_data.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_screen.dart';
import 'package:flutter/widgets.dart';
class ChatThreadExample extends StatelessWidget {
const ChatThreadExample({super.key});
@override
Widget build(BuildContext context) {
return ChatScreen.standalone(
title: sampleChatPerson.name,
service: SampleChatService(latency: const Duration(milliseconds: 600), clock: () => sampleChatNow),
initial: ChatThread(messages: sampleChatMessages),
onBack: () {},
clock: () => sampleChatNow,
);
}
}Sending
Messages written offline wait to be sent, a notice says so, and the other person is typing.
import 'package:your_app/ui/fliin/screens/chat/chat_controller.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_data.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_screen.dart';
import 'package:flutter/widgets.dart';
class ChatSendingExample extends StatelessWidget {
const ChatSendingExample({super.key});
@override
Widget build(BuildContext context) {
return ChatScreen.standalone(
title: sampleChatPerson.name,
service: SampleChatService(latency: const Duration(milliseconds: 600), clock: () => sampleChatNow),
initial: ChatThread(
messages: sampleChatSending,
typing: const [sampleChatPerson],
notice: ChatNotice.sendOffline,
),
onBack: () {},
clock: () => sampleChatNow,
);
}
}Loading
While the service answers, placeholders keep the shape of the messages.
import 'package:your_app/ui/fliin/screens/chat/chat_controller.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_data.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_screen.dart';
import 'package:flutter/widgets.dart';
class ChatLoadingExample extends StatelessWidget {
const ChatLoadingExample({super.key});
@override
Widget build(BuildContext context) {
return ChatScreen.standalone(
title: sampleChatPerson.name,
service: SampleChatService(latency: const Duration(milliseconds: 600), clock: () => sampleChatNow),
initial: const ChatLoading(),
onBack: () {},
clock: () => sampleChatNow,
);
}
}Empty
A conversation with no message yet: the composer waits for the first one.
import 'package:your_app/ui/fliin/screens/chat/chat_controller.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_data.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_screen.dart';
import 'package:flutter/widgets.dart';
class ChatEmptyExample extends StatelessWidget {
const ChatEmptyExample({super.key});
@override
Widget build(BuildContext context) {
return ChatScreen.standalone(
title: sampleChatPerson.name,
service: SampleChatService(latency: const Duration(milliseconds: 600), clock: () => sampleChatNow),
initial: const ChatThread(messages: []),
onBack: () {},
clock: () => sampleChatNow,
);
}
}Error
When loading fails, the screen says so and offers to try again.
import 'package:your_app/ui/fliin/screens/chat/chat_controller.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_data.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_screen.dart';
import 'package:flutter/widgets.dart';
class ChatErrorExample extends StatelessWidget {
const ChatErrorExample({super.key});
@override
Widget build(BuildContext context) {
return ChatScreen.standalone(
title: sampleChatPerson.name,
service: SampleChatService(latency: const Duration(milliseconds: 600), clock: () => sampleChatNow),
initial: const ChatFailed(),
onBack: () {},
clock: () => sampleChatNow,
);
}
}Offline
Without a connection, the screen asks to reconnect rather than showing an error.
import 'package:your_app/ui/fliin/screens/chat/chat_controller.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_data.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_screen.dart';
import 'package:flutter/widgets.dart';
class ChatOfflineExample extends StatelessWidget {
const ChatOfflineExample({super.key});
@override
Widget build(BuildContext context) {
return ChatScreen.standalone(
title: sampleChatPerson.name,
service: SampleChatService(latency: const Duration(milliseconds: 600), clock: () => sampleChatNow),
initial: const ChatOffline(),
onBack: () {},
clock: () => sampleChatNow,
);
}
}About
A conversation about an order: header pins the order under the bar, actions adds a search to the bar, beside the avatar.
import 'package:your_app/ui/fliin/components/app_bar/fl_app_bar.dart';
import 'package:your_app/ui/fliin/components/list_row/fl_list_row.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_controller.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_data.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_screen.dart';
import 'package:flutter/widgets.dart';
class ChatAboutExample extends StatelessWidget {
const ChatAboutExample({super.key});
@override
Widget build(BuildContext context) {
return ChatScreen.standalone(
title: sampleChatPerson.name,
initials: sampleChatPerson.initials,
service: SampleChatService(latency: const Duration(milliseconds: 600), clock: () => sampleChatNow),
initial: ChatThread(messages: sampleChatMessages),
actions: [FlAppBarAction(icon: FlIcon(FlIcons.magnifier), label: 'Search the conversation', onPressed: () {})],
header: FlListRow(
leading: FlIcon(FlIcons.shoppingCart),
title: 'Order FL-2026-10482',
subtitle: 'Delivered today · 137,90 €',
trailing: const FlListRowChevron(),
onPressed: () {},
),
onBack: () {},
onProfile: () {},
clock: () => sampleChatNow,
);
}
}Routing
fliin add chat prints the route for the router it finds in your pubspec, a file next to the page that wires its navigation. Each of them:
go_router
// lib/chat_route.dart, next to chat_page.dart.
import 'package:go_router/go_router.dart';
import 'chat_page.dart';
/// Add chatRoute to the routes of your GoRouter.
final chatRoute = GoRoute(
path: '/chat',
builder: (context, state) => ChatScreenPage(
// Alone in the stack (deep link, go): your app's home.
onBack: () => context.canPop() ? context.pop() : context.go('/'),
),
);navigator
// lib/chat_route.dart, next to chat_page.dart.
import 'package:flutter/material.dart';
import 'chat_page.dart';
/// Opens “Chat thread”: `Navigator.of(context).push(chatRoute())`.
Route<void> chatRoute() => MaterialPageRoute<void>(
builder: (context) => ChatScreenPage(
onBack: () => Navigator.of(context).maybePop(),
),
);State
The controller is a ChangeNotifier from widgets.dart, with no package. fliin add chat prints the page that holds it with the state management it finds, its imports written for your package; below they read your_app. Your backend implements the Service interface of the screen's _data.dart file:
none
// lib/chat_page.dart
// Your backend: implement ChatService (chat_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/chat/chat_data.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_screen.dart';
import 'package:flutter/widgets.dart';
class ChatScreenPage extends StatelessWidget {
const ChatScreenPage({
super.key,
required this.onBack,
});
final VoidCallback onBack;
@override
Widget build(BuildContext context) => ChatScreen.standalone(
title: sampleChatPerson.name,
service: SampleChatService(),
onBack: onBack,
);
}provider
// lib/chat_page.dart
// Your backend: implement ChatService (chat_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/chat/chat_controller.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_data.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class ChatScreenPage extends StatelessWidget {
const ChatScreenPage({
super.key,
required this.onBack,
});
final VoidCallback onBack;
@override
Widget build(BuildContext context) => ChangeNotifierProvider(
create: (_) => ChatController(service: SampleChatService())..load(),
child: Builder(
builder: (context) => ChatScreen(
title: sampleChatPerson.name,
controller: context.read<ChatController>(),
onBack: onBack,
),
),
);
}riverpod
// lib/chat_page.dart
// Your backend: implement ChatService (chat_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/chat/chat_controller.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_data.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final chatProvider = Provider.autoDispose<ChatController>((ref) {
final controller = ChatController(service: SampleChatService())..load();
ref.onDispose(controller.dispose);
return controller;
});
class ChatScreenPage extends ConsumerWidget {
const ChatScreenPage({
super.key,
required this.onBack,
});
final VoidCallback onBack;
@override
Widget build(BuildContext context, WidgetRef ref) => ChatScreen(
title: sampleChatPerson.name,
controller: ref.watch(chatProvider),
onBack: onBack,
);
}bloc
// lib/chat_page.dart
// Your backend: implement ChatService (chat_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/chat/chat_controller.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_data.dart';
import 'package:your_app/ui/fliin/screens/chat/chat_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// The screen's controller as a bloc, for your BlocListener and BlocBuilder:
/// one emission per controller notification. `version` tells them apart
/// when `state` does not change (mode, filters, text…, read them on
/// `controller`). The screen listens to the controller itself.
class ChatCubit extends Cubit<({ChatState state, int version})> {
ChatCubit(this.controller) : super((state: controller.state, version: 0)) {
controller.addListener(_changed);
}
final ChatController controller;
void _changed() => emit((state: controller.state, version: state.version + 1));
@override
Future<void> close() {
controller
..removeListener(_changed)
..dispose();
return super.close();
}
}
class ChatScreenPage extends StatelessWidget {
const ChatScreenPage({
super.key,
required this.onBack,
});
final VoidCallback onBack;
@override
Widget build(BuildContext context) => BlocProvider(
create: (_) => ChatCubit(ChatController(service: SampleChatService())..load()),
child: Builder(
builder: (context) => ChatScreen(
title: sampleChatPerson.name,
controller: context.read<ChatCubit>().controller,
onBack: onBack,
),
),
);
}API
ChatScreen: class extends StatefulWidget
ChatScreen.new: const new({super.key, required String title, required ChatController controller, bool group, String? subtitle, String? initials, ImageProvider? avatar, VoidCallback? onBack, VoidCallback? onProfile, VoidCallback? onAttach, DateTime Function() clock, List<FlAppBarAction> actions, Widget? header})
ChatScreen.standalone: const new({super.key, required String title, required ChatService service, ChatState? initial, bool group, String? subtitle, String? initials, ImageProvider? avatar, VoidCallback? onBack, VoidCallback? onProfile, VoidCallback? onAttach, DateTime Function() clock, List<FlAppBarAction> actions, Widget? header})
ChatScreen.title: get String
ChatScreen.controller: get ChatController?
ChatScreen.service: get ChatService?
ChatScreen.initial: get ChatState?
ChatScreen.group: get bool
ChatScreen.subtitle: get String?
ChatScreen.initials: get String?
ChatScreen.avatar: get ImageProvider?
ChatScreen.onBack: get VoidCallback?
ChatScreen.onProfile: get VoidCallback?
ChatScreen.onAttach: get VoidCallback?
ChatScreen.clock: get DateTime Function()
ChatScreen.actions: get List<FlAppBarAction>
ChatScreen.header: get Widget?
ChatScreen.createState: State<ChatScreen>()
ChatState: sealed class permits {ChatFailed, ChatLoading, ChatOffline, ChatThread}
ChatState.new: const new()
ChatLoading: final class extends ChatState
ChatLoading.new: const new()
ChatNotice: enum {sendOffline, sendFailed, deleteFailed}
ChatNotice.sendOffline: static final ChatNotice
ChatNotice.sendFailed: static final ChatNotice
ChatNotice.deleteFailed: static final ChatNotice
ChatThread: final class extends ChatState
ChatThread.new: const new({required List<ChatMessage> messages, bool hasOlder, bool loadingOlder, bool olderFailed, List<ChatPerson> typing, ChatNotice? notice, ReceivedMessage? arrival})
ChatThread.messages: get List<ChatMessage>
ChatThread.hasOlder: get bool
ChatThread.loadingOlder: get bool
ChatThread.olderFailed: get bool
ChatThread.typing: get List<ChatPerson>
ChatThread.notice: get ChatNotice?
ChatThread.arrival: get ReceivedMessage?
ChatFailed: final class extends ChatState
ChatFailed.new: const new()
ChatOffline: final class extends ChatState
ChatOffline.new: const new()
ChatController: class extends ChangeNotifier
ChatController.new: new({required ChatService service, ChatState initial, DateTime Function() clock, String Function()? newLocalId})
ChatController.service: get ChatService
ChatController.clock: get DateTime Function()
ChatController.newLocalId: get String Function()
ChatController.state: get ChatState
ChatController.load: Future<void>()
ChatController.loadOlder: Future<bool>()
ChatController.send: Future<bool>(String text, {ChatQuote? replyTo})
ChatController.retry: Future<bool>(String id)
ChatController.delete: Future<bool>(String id)
ChatController.dispose: void()
ChatPerson: final class
ChatPerson.new: const new({required String id, required String name, required String initials})
ChatPerson.id: get String
ChatPerson.name: get String
ChatPerson.initials: get String
ChatDelivery: enum {sending, sent, read, failed}
ChatDelivery.sending: static final ChatDelivery
ChatDelivery.sent: static final ChatDelivery
ChatDelivery.read: static final ChatDelivery
ChatDelivery.failed: static final ChatDelivery
ChatQuote: final class
ChatQuote.new: const new({required String messageId, required String text, ChatPerson? author})
ChatQuote.messageId: get String
ChatQuote.text: get String
ChatQuote.author: get ChatPerson?
ChatMessage: sealed class permits {ReceivedMessage, SentMessage}
ChatMessage.new: const new({required String id, required String text, required DateTime at, ChatQuote? replyTo})
ChatMessage.id: get String
ChatMessage.text: get String
ChatMessage.at: get DateTime
ChatMessage.replyTo: get ChatQuote?
ChatMessage.quote: abstract get ChatQuote
ReceivedMessage: final class extends ChatMessage
ReceivedMessage.new: const new({required super.id, required ChatPerson author, required super.text, required super.at, super.replyTo})
ReceivedMessage.author: get ChatPerson
ReceivedMessage.quote: get ChatQuote
SentMessage: final class extends ChatMessage
SentMessage.new: const new({required super.id, required super.text, required super.at, required ChatDelivery delivery, super.replyTo})
SentMessage.delivery: get ChatDelivery
SentMessage.quote: get ChatQuote
SentMessage.copyWith: SentMessage({ChatDelivery? delivery, String? id, DateTime? at})
ChatPage: final class
ChatPage.new: const new({required List<ChatMessage> messages, required bool hasOlder})
ChatPage.messages: get List<ChatMessage>
ChatPage.hasOlder: get bool
ChatDraft: final class
ChatDraft.new: const new({required String localId, required String text, ChatQuote? replyTo})
ChatDraft.localId: get String
ChatDraft.text: get String
ChatDraft.replyTo: get ChatQuote?
ChatEvent: sealed class permits {ChatMessageArrived, ChatReadUntil, ChatTyping}
ChatEvent.new: const new()
ChatMessageArrived: final class extends ChatEvent
ChatMessageArrived.new: const new(ReceivedMessage message)
ChatMessageArrived.message: get ReceivedMessage
ChatTyping: final class extends ChatEvent
ChatTyping.new: const new(List<ChatPerson> people)
ChatTyping.people: get List<ChatPerson>
ChatReadUntil: final class extends ChatEvent
ChatReadUntil.new: const new(DateTime until)
ChatReadUntil.until: get DateTime
ChatRow: sealed class permits {ChatDayRow, ChatMessageRow}
ChatRow.new: const new()
ChatDayRow: final class extends ChatRow
ChatDayRow.new: const new(DateTime day)
ChatDayRow.day: get DateTime
ChatMessageRow: final class extends ChatRow
ChatMessageRow.new: const new(ChatMessage message, {required bool opens, required bool closes})
ChatMessageRow.message: get ChatMessage
ChatMessageRow.opens: get bool
ChatMessageRow.closes: get bool
layoutChat: List<ChatRow>(List<ChatMessage> messages)
ChatNoNetwork: final class implements Exception
ChatNoNetwork.new: const new()
ChatService: abstract interface class
ChatService.new: new()
ChatService.load: abstract Future<ChatPage>()
ChatService.loadOlder: abstract Future<ChatPage>(String beforeId)
ChatService.send: abstract Future<SentMessage>(ChatDraft draft)
ChatService.delete: abstract Future<void>(String id)
ChatService.events: abstract get Stream<ChatEvent>
sampleChatNow: get dynamic
sampleChatPerson: const dynamic
sampleChatMessages: get dynamic
sampleChatSending: get dynamic
SampleChatService: final class implements ChatService
SampleChatService.new: new({List<ChatMessage>? messages, Duration latency, DateTime Function() clock})
SampleChatService.latency: get Duration
SampleChatService.clock: get DateTime Function()
SampleChatService.load: Future<ChatPage>()
SampleChatService.loadOlder: Future<ChatPage>(String beforeId)
SampleChatService.send: Future<SentMessage>(ChatDraft draft)
SampleChatService.delete: Future<void>(String id)
SampleChatService.events: get Stream<ChatEvent>
chatL10n: const Map<String, Map<String, String>>
ChatStrings: final class
ChatStrings.new: const new(String language)
ChatStrings.of: factory new(BuildContext context)
ChatStrings.language: get String
ChatStrings.back: get String
ChatStrings.you: get String
ChatStrings.message: get String
ChatStrings.attach: get String
ChatStrings.profile: String(String name)
ChatStrings.placeholder: get String
ChatStrings.send: get String
ChatStrings.delivery: String(ChatDelivery delivery)
ChatStrings.retry: get String
ChatStrings.actions: get String
ChatStrings.copy: get String
ChatStrings.reply: get String
ChatStrings.delete: get String
ChatStrings.deleteTitle: get String
ChatStrings.deleteMessage: get String
ChatStrings.cancel: get String
ChatStrings.copied: get String
ChatStrings.replyingTo: String(String name)
ChatStrings.inReplyTo: String(String name)
ChatStrings.cancelReply: get String
ChatStrings.typing: String(List<ChatPerson> people)
ChatStrings.arrived: String(ReceivedMessage message)
ChatStrings.time: String(DateTime at)
ChatStrings.day: String(DateTime day, DateTime now)
ChatStrings.dayLabel: String(DateTime day, DateTime now)
ChatStrings.loading: get String
ChatStrings.loadingOlder: get String
ChatStrings.olderFailed: get String
ChatStrings.emptyTitle: get String
ChatStrings.emptyMessage: get String
ChatStrings.errorTitle: get String
ChatStrings.errorMessage: get String
ChatStrings.offlineTitle: get String
ChatStrings.offlineMessage: get String
ChatStrings.notice: String(ChatNotice notice)Strings
Every label in English, French and Arabic. The screen follows the closest Localizations; the same table is in the manifest, ready for your ARB files.
| Key | en | fr | ar |
|---|---|---|---|
chatBack | Back | Retour | رجوع |
chatYou | You | Vous | أنت |
chatMessage | Message | Message | رسالة |
chatAttach | Add an attachment | Ajouter une pièce jointe | إضافة مرفق |
chatProfile | Profile of {name} | Profil de {name} | الملف الشخصي لـ {name} |
chatPlaceholder | Message | Message | اكتب رسالة |
chatSend | Send | Envoyer | إرسال |
chatSending | Sending… | Envoi… | جارٍ الإرسال… |
chatSent | Sent | Envoyé | أُرسلت |
chatRead | Read | Lu | قُرئت |
chatFailed | Not sent | Non envoyé | لم تُرسل |
chatRetry | Try again | Réessayer | أعد المحاولة |
chatActions | Actions | Actions | إجراءات |
chatCopy | Copy | Copier | نسخ |
chatReply | Reply | Répondre | رد |
chatDelete | Delete | Supprimer | حذف |
chatDeleteTitle | Delete this message? | Supprimer ce message ? | حذف هذه الرسالة؟ |
chatDeleteMessage | It will be removed from this conversation. This can’t be undone. | Il sera retiré de la discussion. Cette action est définitive. | ستُزال من هذه المحادثة. لا يمكن التراجع عن ذلك. |
chatCancel | Cancel | Annuler | إلغاء |
chatCopied | Message copied | Message copié | تم نسخ الرسالة |
chatReplyingTo | Replying to {name} | Réponse à {name} | رد على {name} |
chatInReplyTo | In reply to {name} | En réponse à {name} | ردًا على {name} |
chatCancelReply | Cancel reply | Ne pas répondre | إلغاء الرد |
chatTyping | {name} is typing… | {name} écrit… | {name} يكتب… |
chatTypingMany | Several people are typing… | Plusieurs personnes écrivent… | عدة أشخاص يكتبون… |
chatArrived | New message from {name}: {text} | Nouveau message de {name} : {text} | رسالة جديدة من {name}: {text} |
chatToday | Today | Aujourd’hui | اليوم |
chatYesterday | Yesterday | Hier | أمس |
chatMonday | Monday | Lundi | الاثنين |
chatTuesday | Tuesday | Mardi | الثلاثاء |
chatWednesday | Wednesday | Mercredi | الأربعاء |
chatThursday | Thursday | Jeudi | الخميس |
chatFriday | Friday | Vendredi | الجمعة |
chatSaturday | Saturday | Samedi | السبت |
chatSunday | Sunday | Dimanche | الأحد |
chatDate | {month}/{day}/{year} | {day}/{month}/{year} | {day}/{month}/{year} |
chatLoading | Loading messages | Chargement des messages | جارٍ تحميل الرسائل |
chatLoadingOlder | Loading earlier messages | Chargement des messages précédents | جارٍ تحميل الرسائل السابقة |
chatOlderFailed | Earlier messages could not load. | Impossible de charger les messages précédents. | تعذّر تحميل الرسائل السابقة. |
chatEmptyTitle | No messages yet | Aucun message | لا توجد رسائل |
chatEmptyMessage | Write the first message below. | Écrivez le premier message ci-dessous. | اكتب الرسالة الأولى في الأسفل. |
chatErrorTitle | This conversation could not load | Impossible de charger la discussion | تعذّر تحميل المحادثة |
chatErrorMessage | Something went wrong on our side. | Un problème est survenu de notre côté. | حدثت مشكلة من جهتنا. |
chatOfflineTitle | You are offline | Vous êtes hors ligne | أنت غير متصل |
chatOfflineMessage | Check your connection, then try again. Your draft is kept. | Vérifiez votre connexion, puis réessayez. Votre brouillon est gardé. | تحقق من اتصالك ثم أعد المحاولة. مسودتك محفوظة. |
chatSendOffline | You are offline. Tap “Try again” on a message to resend it. | Vous êtes hors ligne. Touchez « Réessayer » sur un message pour le renvoyer. | أنت غير متصل. اضغط «أعد المحاولة» على الرسالة لإعادة إرسالها. |
chatSendFailed | A message was not sent. Tap “Try again” to resend it. | Un message n’est pas parti. Touchez « Réessayer » pour le renvoyer. | لم تُرسل إحدى الرسائل. اضغط «أعد المحاولة» لإعادة إرسالها. |
chatDeleteFailed | The message could not be deleted. | Le message n’a pas pu être supprimé. | تعذّر حذف الرسالة. |
Reference renders
The 16 images published with version 0.2.4, rendered by the shared harness on macOS with Flutter 3.47.7. Each shows every state side by side, at the size of an iPhone (390 × 844) or of an Android phone (412 × 915). fliin verify chat compares your copy to them.
















Use it to
- show the message history of a one-to-one or group chat, day by day
- send a message that shows at once then turns sent or read
- keep messages in order when several are sent in a row
- show a typing indicator while the other person writes
- show the quoted message above a reply
- Not for
- the list of all discussions: use the conversations list (conversations)
- a comment thread under some content: compose message bubbles (message-bubble)
- States