Fliin
On this page

Screens / messaging

Conversations

Lists the conversations of a messaging app, pinned first, with search, pull to refresh, a new conversation, archive, mute and delete, and its empty, loading, error and offline states.

16 of 16 variants pass
Conversations, loaded state
Platform
Brightness
Text scale
Direction

Import

Conversations 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 conversations

Also installs: alert, app-bar, avatar, confirm-delete, conversation-row, core, empty-state, icons, page, search-field, separator, skeleton, text-field

Examples

Loaded

The list of conversations, sorted with the pinned ones first. standalone holds its own controller over the service you pass; give it yours to reach your backend.

Conversations, loaded state
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';

class ConversationsLoadedExample extends StatelessWidget {
  const ConversationsLoadedExample({super.key});

  @override
  Widget build(BuildContext context) {
    return ConversationsScreen.standalone(
      service: SampleConversationsService(latency: const Duration(milliseconds: 600)),
      initial: ConversationsLoaded(sortConversations(sampleConversations)),
      onNewConversation: () {},
      onOpen: (_) {},
      clock: () => sampleConversationsNow,
    );
  }
}

Loading

While the service answers, placeholders keep the shape of the rows.

Conversations, loading state
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';

class ConversationsLoadingExample extends StatelessWidget {
  const ConversationsLoadingExample({super.key});

  @override
  Widget build(BuildContext context) {
    return ConversationsScreen.standalone(
      service: SampleConversationsService(latency: const Duration(milliseconds: 600)),
      initial: const ConversationsLoading(),
      onNewConversation: () {},
      onOpen: (_) {},
      clock: () => sampleConversationsNow,
    );
  }
}

Empty

No conversation yet: the screen invites the user to start one.

Conversations, empty state
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';

class ConversationsEmptyExample extends StatelessWidget {
  const ConversationsEmptyExample({super.key});

  @override
  Widget build(BuildContext context) {
    return ConversationsScreen.standalone(
      service: SampleConversationsService(latency: const Duration(milliseconds: 600)),
      initial: const ConversationsEmpty(),
      onNewConversation: () {},
      onOpen: (_) {},
      clock: () => sampleConversationsNow,
    );
  }
}

Error

When loading fails, the screen says so and offers to try again.

Conversations, error state
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';

class ConversationsErrorExample extends StatelessWidget {
  const ConversationsErrorExample({super.key});

  @override
  Widget build(BuildContext context) {
    return ConversationsScreen.standalone(
      service: SampleConversationsService(latency: const Duration(milliseconds: 600)),
      initial: const ConversationsFailed(),
      onNewConversation: () {},
      onOpen: (_) {},
      clock: () => sampleConversationsNow,
    );
  }
}

Offline

Without a connection, the screen asks to reconnect rather than showing an error.

Conversations, offline state
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';

class ConversationsOfflineExample extends StatelessWidget {
  const ConversationsOfflineExample({super.key});

  @override
  Widget build(BuildContext context) {
    return ConversationsScreen.standalone(
      service: SampleConversationsService(latency: const Duration(milliseconds: 600)),
      initial: const ConversationsOffline(),
      onNewConversation: () {},
      onOpen: (_) {},
      clock: () => sampleConversationsNow,
    );
  }
}

Photos

Each person with a photo (ConversationPerson.avatar), the initials standing in while it loads, and no search above a short inbox (showSearch: false).

Conversations, photos state
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';

class ConversationsPhotosExample extends StatelessWidget {
  const ConversationsPhotosExample({super.key});

  @override
  Widget build(BuildContext context) {
    var n = 0;
    ConversationPerson withPhoto(ConversationPerson person) => ConversationPerson(
      id: person.id,
      name: person.name,
      initials: person.initials,
      color: person.color,
      avatar: AssetImage('images/people/${n++ % 6 + 1}.jpg'),
    );
    final conversations = [
      for (final c in sortConversations(sampleConversations))
        Conversation(
          id: c.id,
          title: c.title,
          people: [for (final person in c.people) withPhoto(person)],
          lastMessage: c.lastMessage,
          lastAt: c.lastAt,
          unread: c.unread,
          muted: c.muted,
          pinned: c.pinned,
        ),
    ];
    return ConversationsScreen.standalone(
      service: SampleConversationsService(latency: const Duration(milliseconds: 600)),
      initial: ConversationsLoaded(conversations),
      showSearch: false,
      onNewConversation: () {},
      onOpen: (_) {},
      clock: () => sampleConversationsNow,
    );
  }
}

Routing

fliin add conversations 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/conversations_route.dart, next to conversations_page.dart.
import 'package:go_router/go_router.dart';

import 'conversations_page.dart';

/// Add conversationsRoute to the routes of your GoRouter.
final conversationsRoute = GoRoute(
  path: '/conversations',
  builder: (context, state) => ConversationsPage(
    // The path of a chat in your app.
    onOpen: (conversation) => context.push('/conversations/${Uri.encodeComponent(conversation.id)}'),
    // The path of a new conversation in your app, outside /conversations/:id.
    onNewConversation: () => context.push('/compose'),
  ),
);

navigator

// lib/conversations_route.dart, next to conversations_page.dart.
import 'package:flutter/material.dart';

import 'conversations_page.dart';

/// Opens “Conversations”: `Navigator.of(context).push(conversationsRoute(conversationPage: …, newConversationPage: …))`.
/// [conversationPage] builds the chat of a conversation in your app.
/// [newConversationPage] builds the new conversation of your app.
Route<void> conversationsRoute({
  required Widget Function(String conversationId) conversationPage,
  required WidgetBuilder newConversationPage,
}) => MaterialPageRoute<void>(
  builder: (context) => ConversationsPage(
    onOpen: (conversation) => Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => conversationPage(conversation.id))),
    onNewConversation: () => Navigator.of(context).push(MaterialPageRoute<void>(builder: newConversationPage)),
  ),
);

State

The controller is a ChangeNotifier from widgets.dart, with no package. fliin add conversations 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/conversations_page.dart
// Your backend: implement ConversationsService (conversations_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';

class ConversationsPage extends StatelessWidget {
  const ConversationsPage({
    super.key,
    required this.onOpen,
    required this.onNewConversation,
  });

  final ValueChanged<Conversation> onOpen;
  final VoidCallback onNewConversation;

  @override
  Widget build(BuildContext context) => ConversationsScreen.standalone(
    service: SampleConversationsService(),
    onOpen: onOpen,
    onNewConversation: onNewConversation,
  );
}

provider

// lib/conversations_page.dart
// Your backend: implement ConversationsService (conversations_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';

class ConversationsPage extends StatelessWidget {
  const ConversationsPage({
    super.key,
    required this.onOpen,
    required this.onNewConversation,
  });

  final ValueChanged<Conversation> onOpen;
  final VoidCallback onNewConversation;

  @override
  Widget build(BuildContext context) => ChangeNotifierProvider(
    create: (_) => ConversationsController(service: SampleConversationsService())..load(),
    child: Builder(
      builder: (context) => ConversationsScreen(
        controller: context.read<ConversationsController>(),
        onOpen: onOpen,
        onNewConversation: onNewConversation,
      ),
    ),
  );
}

riverpod

// lib/conversations_page.dart
// Your backend: implement ConversationsService (conversations_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final conversationsProvider = Provider.autoDispose<ConversationsController>((ref) {
  final controller = ConversationsController(service: SampleConversationsService())..load();
  ref.onDispose(controller.dispose);
  return controller;
});

class ConversationsPage extends ConsumerWidget {
  const ConversationsPage({
    super.key,
    required this.onOpen,
    required this.onNewConversation,
  });

  final ValueChanged<Conversation> onOpen;
  final VoidCallback onNewConversation;

  @override
  Widget build(BuildContext context, WidgetRef ref) => ConversationsScreen(
    controller: ref.watch(conversationsProvider),
    onOpen: onOpen,
    onNewConversation: onNewConversation,
  );
}

bloc

// lib/conversations_page.dart
// Your backend: implement ConversationsService (conversations_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/conversations/conversations_controller.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_data.dart';
import 'package:your_app/ui/fliin/screens/conversations/conversations_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 ConversationsCubit extends Cubit<({ConversationsState state, int version})> {
  ConversationsCubit(this.controller) : super((state: controller.state, version: 0)) {
    controller.addListener(_changed);
  }

  final ConversationsController controller;

  void _changed() => emit((state: controller.state, version: state.version + 1));

  @override
  Future<void> close() {
    controller
      ..removeListener(_changed)
      ..dispose();
    return super.close();
  }
}

class ConversationsPage extends StatelessWidget {
  const ConversationsPage({
    super.key,
    required this.onOpen,
    required this.onNewConversation,
  });

  final ValueChanged<Conversation> onOpen;
  final VoidCallback onNewConversation;

  @override
  Widget build(BuildContext context) => BlocProvider(
    create: (_) => ConversationsCubit(ConversationsController(service: SampleConversationsService())..load()),
    child: Builder(
      builder: (context) => ConversationsScreen(
        controller: context.read<ConversationsCubit>().controller,
        onOpen: onOpen,
        onNewConversation: onNewConversation,
      ),
    ),
  );
}

API

ConversationsScreen: class extends StatefulWidget
ConversationsScreen.new: const new({super.key, required ConversationsController controller, VoidCallback? onBack, ValueChanged<Conversation>? onOpen, VoidCallback? onNewConversation, DateTime Function() clock, bool showSearch})
ConversationsScreen.standalone: const new({super.key, required ConversationsService service, ConversationsState? initial, VoidCallback? onBack, ValueChanged<Conversation>? onOpen, VoidCallback? onNewConversation, DateTime Function() clock, bool showSearch})
ConversationsScreen.controller: get ConversationsController?
ConversationsScreen.service: get ConversationsService?
ConversationsScreen.initial: get ConversationsState?
ConversationsScreen.onBack: get VoidCallback?
ConversationsScreen.onOpen: get ValueChanged<Conversation>?
ConversationsScreen.onNewConversation: get VoidCallback?
ConversationsScreen.clock: get DateTime Function()
ConversationsScreen.showSearch: get bool
ConversationsScreen.createState: State<ConversationsScreen>()
ConversationsState: sealed class permits {ConversationsEmpty, ConversationsFailed, ConversationsLoaded, ConversationsLoading, ConversationsOffline}
ConversationsState.new: const new()
ConversationsLoading: final class extends ConversationsState
ConversationsLoading.new: const new()
ConversationsNotice: enum {refreshFailed, refreshOffline, actionFailed}
ConversationsNotice.refreshFailed: static final ConversationsNotice
ConversationsNotice.refreshOffline: static final ConversationsNotice
ConversationsNotice.actionFailed: static final ConversationsNotice
ConversationsLoaded: final class extends ConversationsState
ConversationsLoaded.new: new(List<Conversation> conversations, {ConversationsNotice? notice})
ConversationsLoaded.conversations: get List<Conversation>
ConversationsLoaded.notice: get ConversationsNotice?
ConversationsEmpty: final class extends ConversationsState
ConversationsEmpty.new: const new({ConversationsNotice? notice})
ConversationsEmpty.notice: get ConversationsNotice?
ConversationsFailed: final class extends ConversationsState
ConversationsFailed.new: const new()
ConversationsOffline: final class extends ConversationsState
ConversationsOffline.new: const new()
ConversationsController: class extends ChangeNotifier
ConversationsController.new: new({required ConversationsService service, ConversationsState initial})
ConversationsController.service: get ConversationsService
ConversationsController.state: get ConversationsState
ConversationsController.load: Future<void>()
ConversationsController.refresh: Future<bool>()
ConversationsController.archive: Future<bool>(String id)
ConversationsController.delete: Future<bool>(String id)
ConversationsController.setMuted: Future<bool>(String id, bool muted)
ConversationsController.dispose: void()
ConversationPerson: final class
ConversationPerson.new: const new({required String id, required String name, required String initials, FlAvatarColor color, ImageProvider? avatar})
ConversationPerson.id: get String
ConversationPerson.name: get String
ConversationPerson.initials: get String
ConversationPerson.color: get FlAvatarColor
ConversationPerson.avatar: get ImageProvider?
Conversation: final class
Conversation.new: const new({required String id, required String title, required List<ConversationPerson> people, required String lastMessage, required DateTime lastAt, int unread, bool muted, bool pinned})
Conversation.id: get String
Conversation.title: get String
Conversation.people: get List<ConversationPerson>
Conversation.lastMessage: get String
Conversation.lastAt: get DateTime
Conversation.unread: get int
Conversation.muted: get bool
Conversation.pinned: get bool
Conversation.isGroup: get bool
Conversation.withMuted: Conversation(bool muted)
sortConversations: List<Conversation>(Iterable<Conversation> conversations)
searchConversations: List<Conversation>(List<Conversation> conversations, String query)
ConversationMoment: sealed class permits {MomentDate, MomentMinutes, MomentNow, MomentToday, MomentWeekday, MomentYesterday}
ConversationMoment.new: const new()
MomentNow: final class extends ConversationMoment
MomentNow.new: const new()
MomentMinutes: final class extends ConversationMoment
MomentMinutes.new: const new(int minutes)
MomentMinutes.minutes: get int
MomentToday: final class extends ConversationMoment
MomentToday.new: const new(DateTime at)
MomentToday.at: get DateTime
MomentYesterday: final class extends ConversationMoment
MomentYesterday.new: const new()
MomentWeekday: final class extends ConversationMoment
MomentWeekday.new: const new(int weekday)
MomentWeekday.weekday: get int
MomentDate: final class extends ConversationMoment
MomentDate.new: const new(DateTime at)
MomentDate.at: get DateTime
conversationMoment: ConversationMoment(DateTime at, DateTime now)
ConversationsNoNetwork: final class implements Exception
ConversationsNoNetwork.new: const new()
ConversationsService: abstract interface class
ConversationsService.new: new()
ConversationsService.load: abstract Future<List<Conversation>>()
ConversationsService.archive: abstract Future<void>(String id)
ConversationsService.setMuted: abstract Future<void>(String id, bool muted)
ConversationsService.delete: abstract Future<void>(String id)
sampleConversationsNow: get dynamic
sampleConversations: get dynamic
SampleConversationsService: final class implements ConversationsService
SampleConversationsService.new: new({List<Conversation>? conversations, Duration latency})
SampleConversationsService.latency: get Duration
SampleConversationsService.load: Future<List<Conversation>>()
SampleConversationsService.archive: Future<void>(String id)
SampleConversationsService.delete: Future<void>(String id)
SampleConversationsService.setMuted: Future<void>(String id, bool muted)
conversationsL10n: const Map<String, Map<String, String>>
ConversationsStrings: final class
ConversationsStrings.new: const new(String language)
ConversationsStrings.of: factory new(BuildContext context)
ConversationsStrings.language: get String
ConversationsStrings.title: get String
ConversationsStrings.back: get String
ConversationsStrings.refresh: get String
ConversationsStrings.newShort: get String
ConversationsStrings.newConversation: get String
ConversationsStrings.search: get String
ConversationsStrings.clearSearch: get String
ConversationsStrings.loading: get String
ConversationsStrings.unread: String(int count)
ConversationsStrings.muted: get String
ConversationsStrings.pinned: get String
ConversationsStrings.actions: get String
ConversationsStrings.archive: get String
ConversationsStrings.mute: get String
ConversationsStrings.unmute: get String
ConversationsStrings.delete: get String
ConversationsStrings.deleteTitle: get String
ConversationsStrings.deleteMessage: String(String name)
ConversationsStrings.cancel: get String
ConversationsStrings.archived: get String
ConversationsStrings.deleted: get String
ConversationsStrings.emptyTitle: get String
ConversationsStrings.emptyMessage: get String
ConversationsStrings.noResultsTitle: get String
ConversationsStrings.noResultsMessage: String(String query)
ConversationsStrings.errorTitle: get String
ConversationsStrings.errorMessage: get String
ConversationsStrings.offlineTitle: get String
ConversationsStrings.offlineMessage: get String
ConversationsStrings.retry: get String
ConversationsStrings.refreshFailed: get String
ConversationsStrings.refreshOffline: get String
ConversationsStrings.actionFailed: get String
ConversationsStrings.moment: String(ConversationMoment moment)

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.

Keyenfrar
conversationsTitleMessagesMessagesالرسائل
conversationsBackBackRetourرجوع
conversationsRefreshRefreshActualiserتحديث
conversationsNewNewNouveauجديدة
conversationsNewConversationNew conversationNouvelle conversationمحادثة جديدة
conversationsSearchSearch conversationsRechercher une conversationالبحث في المحادثات
conversationsClearSearchClear searchEffacer la rechercheمسح البحث
conversationsLoadingLoading conversationsChargement des conversationsجارٍ تحميل المحادثات
conversationsUnreadUnread: {count}Non lus : {count}غير مقروءة: {count}
conversationsMutedMutedEn sourdineمكتومة
conversationsPinnedPinnedÉpingléeمثبّتة
conversationsActionsActionsActionsإجراءات
conversationsArchiveArchiveArchiverأرشفة
conversationsMuteMuteMettre en sourdineكتم
conversationsUnmuteUnmuteRéactiver les notificationsإلغاء الكتم
conversationsDeleteDeleteSupprimerحذف
conversationsDeleteTitleDelete this conversation?Supprimer la conversation ?حذف هذه المحادثة؟
conversationsDeleteMessageAll messages with {name} will be erased. This can’t be undone.Tous les messages avec {name} seront effacés. Cette action est définitive.سيتم مسح كل الرسائل مع {name}. لا يمكن التراجع عن ذلك.
conversationsCancelCancelAnnulerإلغاء
conversationsArchivedConversation archivedConversation archivéeتمت أرشفة المحادثة
conversationsDeletedConversation deletedConversation suppriméeتم حذف المحادثة
conversationsNowNowÀ l’instantالآن
conversationsMinutes{minutes} min{minutes} min{minutes} د
conversationsYesterdayYesterdayHierأمس
conversationsMondayMondaylundiالاثنين
conversationsTuesdayTuesdaymardiالثلاثاء
conversationsWednesdayWednesdaymercrediالأربعاء
conversationsThursdayThursdayjeudiالخميس
conversationsFridayFridayvendrediالجمعة
conversationsSaturdaySaturdaysamediالسبت
conversationsSundaySundaydimancheالأحد
conversationsDate{month}/{day}/{year}{day}/{month}/{year}{day}/{month}/{year}
conversationsEmptyTitleNo conversations yetAucune conversationلا توجد محادثات
conversationsEmptyMessageWrite to someone to start a conversation.Écrivez à quelqu’un pour commencer une conversation.راسل أحدهم لبدء محادثة.
conversationsNoResultsTitleNo resultsAucun résultatلا توجد نتائج
conversationsNoResultsMessageNo conversation matches “{query}”.Aucune conversation ne correspond à « {query} ».لا توجد محادثة تطابق «{query}».
conversationsErrorTitleConversations could not loadImpossible de charger les conversationsتعذّر تحميل المحادثات
conversationsErrorMessageSomething went wrong on our side.Un problème est survenu de notre côté.حدثت مشكلة من جهتنا.
conversationsOfflineTitleYou are offlineVous êtes hors ligneأنت غير متصل
conversationsOfflineMessageCheck your connection, then try again.Vérifiez votre connexion, puis réessayez.تحقق من اتصالك ثم أعد المحاولة.
conversationsRetryTry againRéessayerأعد المحاولة
conversationsRefreshFailedCouldn’t refresh. The list may be out of date.Actualisation impossible. La liste n’est peut-être plus à jour.تعذّر التحديث. قد لا تكون القائمة محدّثة.
conversationsRefreshOfflineYou are offline. The list may be out of date.Vous êtes hors ligne. La liste n’est peut-être plus à jour.أنت غير متصل. قد لا تكون القائمة محدّثة.
conversationsActionFailedThat didn’t work. The conversation is back as it was.L’action n’a pas abouti. La conversation est revenue comme avant.لم يكتمل الإجراء. عادت المحادثة كما كانت.

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 conversations compares your copy to them.

iOS · Light
Conversations, iOS, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Conversations, iOS, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Conversations, iOS, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Conversations, iOS, Light, text 2.0×, Right to left
Text 2.0× · Right to left
iOS · Dark
Conversations, iOS, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Conversations, iOS, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Conversations, iOS, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Conversations, iOS, Dark, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Light
Conversations, Android, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Conversations, Android, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Conversations, Android, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Conversations, Android, Light, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Dark
Conversations, Android, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Conversations, Android, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Conversations, Android, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Conversations, Android, Dark, text 2.0×, Right to left
Text 2.0× · Right to left

Use it to

  • show the messaging inbox, newest chat first
  • select chats whose name or message holds a typed word
  • mute or archive a chat from the list
  • delete a chat after confirmation
  • open a new chat from the top bar
Not for
  • the thread of a single discussion: use the chat thread (chat)
  • received notifications: use the notification center (notifications)
States
  • Loaded
  • Loading
  • Empty
  • Error
  • Offline

Arrows to move, Enter to open, Escape to close.